Fehler beseitigt, Zeitpläne und Status, Export Funktion
einzelne Sortierregeln
This commit is contained in:
parent
e2fe187c5d
commit
adbb1935ec
6 changed files with 206 additions and 56 deletions
7
.claude/settings.json
Executable file
7
.claude/settings.json
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mysql:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
Docker - Image/V 2.2/V 2.2.tar
Executable file
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
BIN
Docker - Image/V 2.3/V 2.3.tar
Executable file
Binary file not shown.
|
|
@ -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
|
||||
# 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
|
||||
# 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
|
||||
|
||||
|
|
|
|||
|
|
@ -465,14 +465,19 @@ def execute_mail_abruf(db, zeitplan: Zeitplan) -> Dict:
|
|||
))
|
||||
|
||||
# Postfach-Status aktualisieren
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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,19 +2549,17 @@ 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>';
|
||||
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 ? '🟢' : '⚪';
|
||||
|
|
@ -2543,30 +2567,74 @@ function renderStatusUebersicht(status) {
|
|||
html += `<div class="status-item">${aktiv} ${escapeHtml(p.name)}: ${letzte} (${p.letzte_anzahl || 0} Dateien)</div>`;
|
||||
}
|
||||
} else {
|
||||
html += '<div class="status-item">Keine Postfächer</div>';
|
||||
html = '<p class="empty-state">Keine Postfächer konfiguriert</p>';
|
||||
}
|
||||
mailabrufContainer.innerHTML = html;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Grobsortierung
|
||||
html += '<div class="status-section"><h4>📁 Grobsortierung</h4>';
|
||||
// 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)}</div>`;
|
||||
html += `<div class="status-item">${aktiv} ${escapeHtml(o.name)}: ${escapeHtml(o.pfad)}</div>`;
|
||||
}
|
||||
} else {
|
||||
html += '<div class="status-item">Keine Ordner</div>';
|
||||
html = '<p class="empty-state">Keine Quellordner konfiguriert</p>';
|
||||
}
|
||||
grobContainer.innerHTML = html;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Scheduler
|
||||
html += '<div class="status-section"><h4>⏰ Scheduler</h4>';
|
||||
const schedulerStatus = status.scheduler?.scheduler_laeuft ? '🟢 Läuft' : '🔴 Gestoppt';
|
||||
// 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>`;
|
||||
html += '</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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue