diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100755 index 0000000..5804bb1 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(mysql:*)" + ] + } +} diff --git a/Docker - Image/V 2.2/V 2.2.tar b/Docker - Image/V 2.2/V 2.2.tar new file mode 100755 index 0000000..4d273ea Binary files /dev/null and b/Docker - Image/V 2.2/V 2.2.tar differ diff --git a/Docker - Image/V 2.3/V 2.3.tar b/Docker - Image/V 2.3/V 2.3.tar new file mode 100755 index 0000000..a686fa7 Binary files /dev/null and b/Docker - Image/V 2.3/V 2.3.tar differ diff --git a/Source/backend/app/routes/api.py b/Source/backend/app/routes/api.py index 2d4b057..dc017c5 100755 --- a/Source/backend/app/routes/api.py +++ b/Source/backend/app/routes/api.py @@ -1307,6 +1307,28 @@ def export_regeln(db: Session = Depends(get_db)): return {"regeln": export_data, "anzahl": len(export_data)} +@router.get("/regeln/{id}/export") +def export_einzelne_regel(id: int, db: Session = Depends(get_db)): + """Exportiert eine einzelne Regel als JSON""" + regel = db.query(SortierRegel).filter(SortierRegel.id == id).first() + if not regel: + raise HTTPException(status_code=404, detail="Regel nicht gefunden") + + export_data = { + "name": regel.name, + "prioritaet": regel.prioritaet, + "muster": regel.muster, + "extraktion": regel.extraktion, + "schema": regel.schema, + "unterordner": regel.unterordner, + "ziel_ordner": regel.ziel_ordner, + "nur_umbenennen": regel.nur_umbenennen, + "ist_fallback": regel.ist_fallback, + "aktiv": regel.aktiv + } + return {"regeln": [export_data], "anzahl": 1, "regel_name": regel.name} + + class RegelnImportRequest(BaseModel): regeln: List[dict] modus: str = "hinzufuegen" # "hinzufuegen", "ersetzen", "aktualisieren" @@ -2578,10 +2600,11 @@ def regel_vorschau(id: int, data: ExtraktionTestRequest, db: Session = Depends(g class ZeitplanCreate(BaseModel): name: str - typ: str # "mail_abruf", "grobsortierung", "sortierregeln" + typ: str # "mail_abruf", "grobsortierung", "sortierregeln", "db_backup" postfach_id: Optional[int] = None quell_ordner_id: Optional[int] = None regel_id: Optional[int] = None # Für typ="sortierregeln" + datenbank_id: Optional[int] = None # Für typ="db_backup" intervall: str # "stündlich", "täglich", "wöchentlich", "monatlich" stunde: int = 6 minute: int = 0 @@ -2597,6 +2620,7 @@ class ZeitplanResponse(BaseModel): postfach_id: Optional[int] quell_ordner_id: Optional[int] regel_id: Optional[int] + datenbank_id: Optional[int] intervall: str stunde: int minute: int @@ -2639,8 +2663,17 @@ def erstelle_zeitplan(data: ZeitplanCreate, db: Session = Depends(get_db)): from ..services.scheduler_service import sync_zeitplaene, trigger_zeitplan_manuell sync_zeitplaene() - # Zeitplan direkt beim Erstellen einmal ausführen - trigger_zeitplan_manuell(zeitplan.id) + # Zeitplan direkt beim Erstellen einmal ausführen (in separatem Thread) + import threading + def run_zeitplan(): + try: + trigger_zeitplan_manuell(zeitplan.id) + except Exception as e: + import logging + logging.getLogger(__name__).warning(f"Fehler bei sofortiger Ausführung: {e}") + + thread = threading.Thread(target=run_zeitplan, daemon=True) + thread.start() return zeitplan @@ -2662,8 +2695,17 @@ def aktualisiere_zeitplan(id: int, data: ZeitplanCreate, db: Session = Depends(g from ..services.scheduler_service import sync_zeitplaene, trigger_zeitplan_manuell sync_zeitplaene() - # Zeitplan direkt beim Speichern einmal ausführen - trigger_zeitplan_manuell(zeitplan.id) + # Zeitplan direkt beim Speichern einmal ausführen (in separatem Thread) + import threading + def run_zeitplan(): + try: + trigger_zeitplan_manuell(zeitplan.id) + except Exception as e: + import logging + logging.getLogger(__name__).warning(f"Fehler bei sofortiger Ausführung: {e}") + + thread = threading.Thread(target=run_zeitplan, daemon=True) + thread.start() return zeitplan diff --git a/Source/backend/app/services/scheduler_service.py b/Source/backend/app/services/scheduler_service.py index 863a9e2..89779ad 100755 --- a/Source/backend/app/services/scheduler_service.py +++ b/Source/backend/app/services/scheduler_service.py @@ -465,14 +465,19 @@ def execute_mail_abruf(db, zeitplan: Zeitplan) -> Dict: )) # Postfach-Status aktualisieren - postfach.letzter_abruf = datetime.utcnow() - postfach.letzte_anzahl = len(ergebnisse) - db.commit() + try: + postfach.letzter_abruf = datetime.utcnow() + postfach.letzte_anzahl = len(ergebnisse) + db.commit() + except Exception as commit_error: + logger.warning(f"Postfach-Status Update fehlgeschlagen: {commit_error}") + db.rollback() gesamt_dateien += len(ergebnisse) fetcher.disconnect() except Exception as e: + db.rollback() fehler.append(f"{postfach.name}: {str(e)[:100]}") if fehler: diff --git a/Source/frontend/static/js/app.js b/Source/frontend/static/js/app.js index 13a2e9a..ce60ee2 100755 --- a/Source/frontend/static/js/app.js +++ b/Source/frontend/static/js/app.js @@ -1166,6 +1166,7 @@ function renderRegeln(regeln) { + @@ -1185,6 +1186,30 @@ async function kopiereRegel(id) { // ============ Regeln Import/Export ============ +async function exportiereEinzelneRegel(id) { + try { + const result = await api(`/regeln/${id}/export`); + const json = JSON.stringify(result.regeln, null, 2); + + // Download als Datei + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + // Dateiname mit Regelname + const fileName = `regel_${result.regel_name.replace(/[^a-zA-Z0-9äöüÄÖÜß_-]/g, '_')}.json`; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + showAlert(`Regel "${result.regel_name}" exportiert`, 'success'); + } catch (error) { + showAlert('Fehler beim Export: ' + error.message, 'error'); + } +} + async function exportiereRegeln() { try { const result = await api('/regeln/export'); @@ -2500,15 +2525,16 @@ function renderZeitplaene(zeitplaene) { const naechste = zp.naechste_ausfuehrung ? formatDatum(zp.naechste_ausfuehrung) : '-'; const letzte = zp.letzte_ausfuehrung ? formatDatum(zp.letzte_ausfuehrung) : 'Noch nie'; + const statusText = zp.letzter_status === 'erfolg' ? 'Erfolg' : (zp.letzter_status === 'fehler' ? 'Fehler' : zp.letzter_status); return `

${typIcon} ${escapeHtml(zp.name)} ${zp.aktiv ? 'Aktiv' : 'Inaktiv'} - ${zp.intervall} + ${formatIntervall(zp.intervall)}

Nächste: ${naechste} | Letzte: ${letzte} - ${zp.letzter_status ? `Status: ${zp.letzter_status}${zp.letzte_meldung ? ' - ' + escapeHtml(zp.letzte_meldung.substring(0, 80)) : ''}` : ''} + ${zp.letzter_status ? `Status: ${statusText}${zp.letzte_meldung ? ' - ' + escapeHtml(zp.letzte_meldung.substring(0, 80)) : ''}` : ''}
@@ -2523,50 +2549,92 @@ function renderZeitplaene(zeitplaene) { async function ladeStatus() { try { const result = await api('/status/uebersicht'); - renderStatusUebersicht(result); + renderStatusFuerTabs(result); } catch (error) { - console.error('Fehler:', error); + console.error('Fehler beim Laden des Status:', error); } } -function renderStatusUebersicht(status) { - const container = document.getElementById('status-uebersicht'); - - let html = '
'; - - // Postfächer - html += '

📧 Postfächer

'; - if (status.postfaecher && status.postfaecher.length > 0) { - for (const p of status.postfaecher) { - const aktiv = p.aktiv ? '🟢' : '⚪'; - const letzte = p.letzter_abruf ? formatDatum(p.letzter_abruf) : 'Nie'; - html += `
${aktiv} ${escapeHtml(p.name)}: ${letzte} (${p.letzte_anzahl || 0} Dateien)
`; +function renderStatusFuerTabs(status) { + // Mailabruf Status + const mailabrufContainer = document.getElementById('status-mailabruf'); + if (mailabrufContainer) { + let html = ''; + if (status.postfaecher && status.postfaecher.length > 0) { + for (const p of status.postfaecher) { + const aktiv = p.aktiv ? '🟢' : '⚪'; + const letzte = p.letzter_abruf ? formatDatum(p.letzter_abruf) : 'Nie'; + html += `
${aktiv} ${escapeHtml(p.name)}: ${letzte} (${p.letzte_anzahl || 0} Dateien)
`; + } + } else { + html = '

Keine Postfächer konfiguriert

'; } - } else { - html += '
Keine Postfächer
'; + mailabrufContainer.innerHTML = html; } - html += '
'; - // Grobsortierung - html += '

📁 Grobsortierung

'; - if (status.quell_ordner && status.quell_ordner.length > 0) { - for (const o of status.quell_ordner) { - const aktiv = o.aktiv ? '🟢' : '⚪'; - html += `
${aktiv} ${escapeHtml(o.name)}
`; + // Grobsortierung Status + const grobContainer = document.getElementById('status-grobsortierung'); + if (grobContainer) { + let html = ''; + if (status.quell_ordner && status.quell_ordner.length > 0) { + for (const o of status.quell_ordner) { + const aktiv = o.aktiv ? '🟢' : '⚪'; + html += `
${aktiv} ${escapeHtml(o.name)}: ${escapeHtml(o.pfad)}
`; + } + } else { + html = '

Keine Quellordner konfiguriert

'; } - } else { - html += '
Keine Ordner
'; + grobContainer.innerHTML = html; } - html += '
'; - // Scheduler - html += '

⏰ Scheduler

'; - const schedulerStatus = status.scheduler?.scheduler_laeuft ? '🟢 Läuft' : '🔴 Gestoppt'; - html += `
${schedulerStatus}
`; - html += '
'; + // Feinsortierung Status + const feinContainer = document.getElementById('status-feinsortierung'); + if (feinContainer) { + let html = ''; + const schedulerStatus = status.scheduler?.scheduler_laeuft ? '🟢 Scheduler läuft' : '🔴 Scheduler gestoppt'; + const regelnZeitplaene = (status.scheduler?.zeitplaene || []).filter(z => z.typ === 'sortierregeln'); + html += `
${schedulerStatus}
`; + if (regelnZeitplaene.length > 0) { + for (const z of regelnZeitplaene) { + const statusIcon = z.letzter_status === 'erfolg' ? '✓' : (z.letzter_status === 'fehler' ? '✗' : '○'); + html += `
${statusIcon} ${escapeHtml(z.name)}: ${z.letzte_meldung || 'Noch nicht ausgeführt'}
`; + } + } + feinContainer.innerHTML = html; + } - html += '
'; - container.innerHTML = html; + // DB Backup Status + const dbContainer = document.getElementById('status-dbbackup'); + if (dbContainer) { + let html = ''; + const schedulerStatus = status.scheduler?.scheduler_laeuft ? '🟢 Scheduler läuft' : '🔴 Scheduler gestoppt'; + const dbZeitplaene = (status.scheduler?.zeitplaene || []).filter(z => z.typ === 'db_backup'); + html += `
${schedulerStatus}
`; + if (dbZeitplaene.length > 0) { + for (const z of dbZeitplaene) { + const statusIcon = z.letzter_status === 'erfolg' ? '✓' : (z.letzter_status === 'fehler' ? '✗' : '○'); + const letzteAusfuehrung = z.letzte_ausfuehrung ? formatDatum(z.letzte_ausfuehrung) : 'Noch nie'; + html += `
${statusIcon} ${escapeHtml(z.name)}
`; + html += `
Letztes Backup: ${letzteAusfuehrung}
`; + if (z.letzte_meldung) { + html += `
${escapeHtml(z.letzte_meldung)}
`; + } + } + } else { + html += '
Keine DB-Backup Zeitpläne konfiguriert
'; + } + dbContainer.innerHTML = html; + } +} + +function formatIntervall(intervall) { + const mapping = { + 'stündlich': 'Stündlich', + 'täglich': 'Täglich', + 'wöchentlich': 'Wöchentlich', + 'monatlich': 'Monatlich' + }; + return mapping[intervall] || intervall; } function formatDatum(isoString) { @@ -2599,6 +2667,7 @@ async function zeigeZeitplanModal(zeitplan = null) { document.getElementById('zp-postfach').value = zeitplan?.postfach_id || ''; document.getElementById('zp-ordner').value = zeitplan?.quell_ordner_id || ''; document.getElementById('zp-regel').value = zeitplan?.regel_id || ''; + document.getElementById('zp-db').value = zeitplan?.datenbank_id || ''; zeitplanTypChanged(); zeitplanIntervallChanged(); @@ -2673,6 +2742,7 @@ async function speichereZeitplan() { const postfachId = document.getElementById('zp-postfach').value; const ordnerId = document.getElementById('zp-ordner').value; const regelId = document.getElementById('zp-regel').value; + const dbId = document.getElementById('zp-db').value; if (data.typ === 'mail_abruf' && postfachId) { data.postfach_id = parseInt(postfachId); @@ -2683,6 +2753,9 @@ async function speichereZeitplan() { if (data.typ === 'sortierregeln' && regelId) { data.regel_id = parseInt(regelId); } + if (data.typ === 'db_backup' && dbId) { + data.datenbank_id = parseInt(dbId); + } if (!data.name) { showAlert('Bitte einen Namen eingeben', 'warning'); @@ -2696,17 +2769,34 @@ async function speichereZeitplan() { await api('/zeitplaene', { method: 'POST', body: JSON.stringify(data) }); } schliesseModal('zeitplan-modal'); - ladeZeitplaene(); + aktualisiereZeitplanListen(); ladeStatus(); } catch (error) { showAlert(error.message, 'error'); } } +function aktualisiereZeitplanListen() { + ladeZeitplaene(); + const aktiverTab = localStorage.getItem('aktiver-tab'); + if (aktiverTab) { + const tabMapping = { + 'mailabruf': ['mail_abruf', 'zeitplaene-mailabruf'], + 'grobsortierung': ['grobsortierung', 'zeitplaene-grobsortierung'], + 'feinsortierung': ['sortierregeln', 'zeitplaene-feinsortierung'], + 'dbbackup': ['db_backup', 'zeitplaene-dbbackup'] + }; + const mapping = tabMapping[aktiverTab]; + if (mapping) { + ladeZeitplaeneNachTyp(mapping[0], mapping[1]); + } + } +} + async function zeitplanAktivieren(id) { try { await api(`/zeitplaene/${id}/aktivieren`, { method: 'POST' }); - ladeZeitplaene(); + aktualisiereZeitplanListen(); } catch (error) { showAlert(error.message, 'error'); } @@ -2717,7 +2807,7 @@ async function zeitplanAusfuehren(id) { zeigeLoading('Führe Zeitplan aus...'); const result = await api(`/zeitplaene/${id}/ausfuehren`, { method: 'POST' }); showAlert(result.meldung || 'Ausgeführt', 'success', 'Zeitplan ausgeführt'); - ladeZeitplaene(); + aktualisiereZeitplanListen(); ladeStatus(); } catch (error) { showAlert(error.message, 'error'); @@ -2739,7 +2829,7 @@ async function zeitplanLoeschen(id) { if (!await showConfirm('Zeitplan wirklich löschen?')) return; try { await api(`/zeitplaene/${id}`, { method: 'DELETE' }); - ladeZeitplaene(); + aktualisiereZeitplanListen(); } catch (error) { showAlert(error.message, 'error'); } @@ -2835,8 +2925,8 @@ function debugLog(message, type = 'info') { async function ladeZeitplaeneNachTyp(typ, containerId) { try { - const zeitplaene = await api('/zeitplaene'); - const gefiltert = zeitplaene.filter(z => z.typ === typ); + const result = await api('/zeitplaene'); + const gefiltert = (result.zeitplaene || []).filter(z => z.typ === typ); const container = document.getElementById(containerId); if (gefiltert.length === 0) { @@ -2844,19 +2934,25 @@ async function ladeZeitplaeneNachTyp(typ, containerId) { return; } - container.innerHTML = gefiltert.map(z => ` -
+ container.innerHTML = gefiltert.map(z => { + const letzteAusfuehrung = z.letzte_ausfuehrung ? formatDatum(z.letzte_ausfuehrung) : 'Noch nie'; + const naechsteAusfuehrung = z.naechste_ausfuehrung ? formatDatum(z.naechste_ausfuehrung) : '-'; + return ` +

${escapeHtml(z.name)} ${z.aktiv ? '✓' : ''}

- ${z.intervall} ${z.stunde != null ? `um ${z.stunde}:${String(z.minute || 0).padStart(2, '0')}` : ''} + ${formatIntervall(z.intervall)} ${z.stunde != null ? `um ${z.stunde}:${String(z.minute || 0).padStart(2, '0')} Uhr` : ''} + Nächste: ${naechsteAusfuehrung} | Letzte: ${letzteAusfuehrung} + ${z.letzter_status ? `Status: ${z.letzter_status === 'erfolg' ? 'Erfolg' : 'Fehler'}${z.letzte_meldung ? ' - ' + escapeHtml(z.letzte_meldung.substring(0, 60)) : ''}` : ''}
- - - + + + +
- `).join(''); + `}).join(''); } catch (error) { console.error('Fehler beim Laden der Zeitpläne:', error); }