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 `
Keine Postfächer konfiguriert
'; } - } else { - html += 'Keine Quellordner konfiguriert
'; } - } else { - html += '