Fehler beseitigt, Zeitpläne und Status, Export Funktion

einzelne Sortierregeln
This commit is contained in:
Eduard Wisch 2026-02-10 19:15:45 +01:00
parent e2fe187c5d
commit adbb1935ec
6 changed files with 206 additions and 56 deletions

7
.claude/settings.json Executable file
View file

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(mysql:*)"
]
}
}

BIN
Docker - Image/V 2.2/V 2.2.tar Executable file

Binary file not shown.

BIN
Docker - Image/V 2.3/V 2.3.tar Executable file

Binary file not shown.

View file

@ -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

View file

@ -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:

View file

@ -1166,6 +1166,7 @@ function renderRegeln(regeln) {
<button class="btn btn-sm" onclick="bearbeiteRegel(${r.id})" title="Bearbeiten"></button>
<button class="btn btn-sm" onclick="regelAktivieren(${r.id})" title="${r.aktiv ? 'Deaktivieren' : 'Aktivieren'}">${r.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm" onclick="kopiereRegel(${r.id})" title="Regel kopieren">📋</button>
<button class="btn btn-sm" onclick="exportiereEinzelneRegel(${r.id})" title="Regel exportieren">📤</button>
<button class="btn btn-sm btn-danger" onclick="regelLoeschen(${r.id})" title="Löschen">×</button>
</div>
</div>
@ -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 `
<div class="config-item" style="${aktivClass}">
<div class="config-item-info">
<h4>${typIcon} ${escapeHtml(zp.name)}
<span class="badge ${zp.aktiv ? 'badge-success' : 'badge-danger'}">${zp.aktiv ? 'Aktiv' : 'Inaktiv'}</span>
<span class="badge badge-info">${zp.intervall}</span>
<span class="badge badge-info">${formatIntervall(zp.intervall)}</span>
</h4>
<small>Nächste: ${naechste} | Letzte: ${letzte}</small>
${zp.letzter_status ? `<small style="display:block;" class="${statusClass}">Status: ${zp.letzter_status}${zp.letzte_meldung ? ' - ' + escapeHtml(zp.letzte_meldung.substring(0, 80)) : ''}</small>` : ''}
${zp.letzter_status ? `<small style="display:block;" class="${statusClass}">Status: ${statusText}${zp.letzte_meldung ? ' - ' + escapeHtml(zp.letzte_meldung.substring(0, 80)) : ''}</small>` : ''}
</div>
<div class="config-item-actions">
<button class="btn btn-sm btn-success" onclick="zeitplanAusfuehren(${zp.id})" title="Jetzt ausführen"></button>
@ -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 = '<div class="status-grid">';
// Postfächer
html += '<div class="status-section"><h4>📧 Postfächer</h4>';
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 += `<div class="status-item">${aktiv} ${escapeHtml(p.name)}: ${letzte} (${p.letzte_anzahl || 0} Dateien)</div>`;
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 += `<div class="status-item">${aktiv} ${escapeHtml(p.name)}: ${letzte} (${p.letzte_anzahl || 0} Dateien)</div>`;
}
} else {
html = '<p class="empty-state">Keine Postfächer konfiguriert</p>';
}
} else {
html += '<div class="status-item">Keine Postfächer</div>';
mailabrufContainer.innerHTML = html;
}
html += '</div>';
// Grobsortierung
html += '<div class="status-section"><h4>📁 Grobsortierung</h4>';
if (status.quell_ordner && status.quell_ordner.length > 0) {
for (const o of status.quell_ordner) {
const aktiv = o.aktiv ? '🟢' : '⚪';
html += `<div class="status-item">${aktiv} ${escapeHtml(o.name)}</div>`;
// 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 += `<div class="status-item">${aktiv} ${escapeHtml(o.name)}: ${escapeHtml(o.pfad)}</div>`;
}
} else {
html = '<p class="empty-state">Keine Quellordner konfiguriert</p>';
}
} else {
html += '<div class="status-item">Keine Ordner</div>';
grobContainer.innerHTML = html;
}
html += '</div>';
// Scheduler
html += '<div class="status-section"><h4>⏰ Scheduler</h4>';
const schedulerStatus = status.scheduler?.scheduler_laeuft ? '🟢 Läuft' : '🔴 Gestoppt';
html += `<div class="status-item">${schedulerStatus}</div>`;
html += '</div>';
// 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 += `<div class="status-item">${schedulerStatus}</div>`;
if (regelnZeitplaene.length > 0) {
for (const z of regelnZeitplaene) {
const statusIcon = z.letzter_status === 'erfolg' ? '✓' : (z.letzter_status === 'fehler' ? '✗' : '○');
html += `<div class="status-item">${statusIcon} ${escapeHtml(z.name)}: ${z.letzte_meldung || 'Noch nicht ausgeführt'}</div>`;
}
}
feinContainer.innerHTML = html;
}
html += '</div>';
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 += `<div class="status-item">${schedulerStatus}</div>`;
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 += `<div class="status-item">${statusIcon} ${escapeHtml(z.name)}</div>`;
html += `<div class="status-item" style="margin-left: 20px; font-size: 0.9em;">Letztes Backup: ${letzteAusfuehrung}</div>`;
if (z.letzte_meldung) {
html += `<div class="status-item" style="margin-left: 20px; font-size: 0.9em;">${escapeHtml(z.letzte_meldung)}</div>`;
}
}
} else {
html += '<div class="status-item">Keine DB-Backup Zeitpläne konfiguriert</div>';
}
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 => `
<div class="config-item">
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 `
<div class="config-item" style="${z.aktiv ? '' : 'opacity: 0.5;'}">
<div class="config-item-info">
<h4>${escapeHtml(z.name)} ${z.aktiv ? '✓' : ''}</h4>
<small>${z.intervall} ${z.stunde != null ? `um ${z.stunde}:${String(z.minute || 0).padStart(2, '0')}` : ''}</small>
<small>${formatIntervall(z.intervall)} ${z.stunde != null ? `um ${z.stunde}:${String(z.minute || 0).padStart(2, '0')} Uhr` : ''}</small>
<small style="display:block;">Nächste: ${naechsteAusfuehrung} | Letzte: ${letzteAusfuehrung}</small>
${z.letzter_status ? `<small style="display:block;" class="${z.letzter_status === 'erfolg' ? 'success' : 'error'}">Status: ${z.letzter_status === 'erfolg' ? 'Erfolg' : 'Fehler'}${z.letzte_meldung ? ' - ' + escapeHtml(z.letzte_meldung.substring(0, 60)) : ''}</small>` : ''}
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="zeitplanAusfuehren(${z.id})" title="Jetzt ausführen"></button>
<button class="btn btn-sm" onclick="zeitplanAktivieren(${z.id})">${z.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm btn-danger" onclick="zeitplanLoeschen(${z.id})">🗑</button>
<button class="btn btn-sm btn-success" onclick="zeitplanAusfuehren(${z.id})" title="Jetzt ausführen"></button>
<button class="btn btn-sm" onclick="zeitplanBearbeiten(${z.id})" title="Bearbeiten"></button>
<button class="btn btn-sm" onclick="zeitplanAktivieren(${z.id})" title="${z.aktiv ? 'Deaktivieren' : 'Aktivieren'}">${z.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm btn-danger" onclick="zeitplanLoeschen(${z.id})" title="Löschen">🗑</button>
</div>
</div>
`).join('');
`}).join('');
} catch (error) {
console.error('Fehler beim Laden der Zeitpläne:', error);
}