V 2.0 - Feinsortierung Live-Streaming, Import/Export, PDF-Rotation
Neue Features: - Feinsortierung mit Live-Streaming (SSE) - zeigt Fortschritt in Echtzeit - Import/Export für Sortierregeln (JSON) - Sortierregeln-Liste mit Sortieroptionen (Name A-Z/Z-A, Priorität) - Checkbox "Auch Dateinamen prüfen" für Keyword-Matching - Automatische PDF-Seitenrotation bei OCR (90°, 180°, 270°) - Tab-Persistenz über Page-Reload (localStorage) - Modals schließen nur noch über X-Button Bugfixes: - Keywords nutzen jetzt Wortgrenzen (\b) - "rechnung" matched nicht mehr "Berechnung" - Keyword-Prüfung standardmäßig nur auf PDF-Text, nicht Dateinamen - Natürliche Sortierung für Regelnamen (1, 2, 10 statt 1, 10, 2) Technisch: - Async SSE-Generator mit asyncio.sleep(0) für sofortiges Streaming - ocrmypdf mit --rotate-pages Flag - Timeout für OCR auf 3 Minuten erhöht Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6e85481f52
commit
c5ee82e1c2
12 changed files with 2382 additions and 100 deletions
|
|
@ -1,15 +1,19 @@
|
|||
# Dateiverwaltung Docker Image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# System-Abhängigkeiten für OCR und PDF
|
||||
# System-Abhängigkeiten für OCR, PDF und DB-Backup
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu \
|
||||
ocrmypdf \
|
||||
unpaper \
|
||||
poppler-utils \
|
||||
ghostscript \
|
||||
libmagic1 \
|
||||
curl \
|
||||
mariadb-client \
|
||||
postgresql-client \
|
||||
gzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Arbeitsverzeichnis
|
||||
|
|
|
|||
|
|
@ -152,10 +152,11 @@ class Zeitplan(Base):
|
|||
aktiv = Column(Boolean, default=True)
|
||||
|
||||
# Was wird ausgeführt
|
||||
typ = Column(String(50), nullable=False) # "mail_abruf", "grobsortierung", "sortierregeln"
|
||||
typ = Column(String(50), nullable=False) # "mail_abruf", "grobsortierung", "sortierregeln", "db_backup"
|
||||
postfach_id = Column(Integer) # Optional: spezifisches Postfach
|
||||
quell_ordner_id = Column(Integer) # Optional: spezifischer Quellordner
|
||||
regel_id = Column(Integer) # Optional: spezifische Regel (für sortierregeln)
|
||||
datenbank_id = Column(Integer) # Optional: spezifische Datenbank (für db_backup)
|
||||
|
||||
# Zeitplan-Intervall
|
||||
intervall = Column(String(20), nullable=False) # "stündlich", "täglich", "wöchentlich", "monatlich"
|
||||
|
|
@ -207,6 +208,53 @@ class VerarbeiteteDatei(Base):
|
|||
verarbeitet_am = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
# ============ BEREICH 3: Datenbank-Backup ============
|
||||
|
||||
class DbServer(Base):
|
||||
"""Datenbank-Server Konfiguration"""
|
||||
__tablename__ = "db_server"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
typ = Column(String(20), nullable=False) # "mariadb", "mysql", "postgresql"
|
||||
host = Column(String(255), nullable=False)
|
||||
port = Column(Integer, default=3306)
|
||||
user = Column(String(100), nullable=False)
|
||||
passwort = Column(String(255), nullable=False)
|
||||
aktiv = Column(Boolean, default=True)
|
||||
erstellt_am = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class Datenbank(Base):
|
||||
"""Datenbank-Konfiguration für Backup"""
|
||||
__tablename__ = "datenbanken"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
server_id = Column(Integer, ForeignKey("db_server.id", ondelete="CASCADE"), nullable=False)
|
||||
database = Column(String(100), nullable=False) # Datenbank-Name
|
||||
backup_pfad = Column(String(500), nullable=False) # Zielordner für Backups
|
||||
aufbewahrung = Column(Integer, default=7) # Anzahl Backups die behalten werden
|
||||
format = Column(String(20), default="sql") # "sql", "sql.gz", "zip"
|
||||
aktiv = Column(Boolean, default=True)
|
||||
letztes_backup = Column(DateTime)
|
||||
letzte_groesse_mb = Column(Integer)
|
||||
erstellt_am = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class BackupLog(Base):
|
||||
"""Log durchgeführter Backups"""
|
||||
__tablename__ = "backup_log"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
datenbank_id = Column(Integer, ForeignKey("datenbanken.id", ondelete="CASCADE"), nullable=False)
|
||||
dateiname = Column(String(500), nullable=False)
|
||||
groesse_bytes = Column(Integer)
|
||||
status = Column(String(50)) # "erfolg", "fehler"
|
||||
fehler = Column(Text)
|
||||
erstellt_am = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
def migrate_db():
|
||||
"""Fügt fehlende Spalten hinzu ohne Daten zu löschen"""
|
||||
from sqlalchemy import inspect, text
|
||||
|
|
@ -240,7 +288,8 @@ def migrate_db():
|
|||
"nur_umbenennen": "BOOLEAN DEFAULT 0"
|
||||
},
|
||||
"zeitplaene": {
|
||||
"regel_id": "INTEGER"
|
||||
"regel_id": "INTEGER",
|
||||
"datenbank_id": "INTEGER"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -444,15 +444,15 @@ class PDFProcessor:
|
|||
"ocrmypdf",
|
||||
"--language", self.ocr_language,
|
||||
"--deskew", # Schräge Scans korrigieren
|
||||
"--rotate-pages", # Automatische Seitenrotation (90°, 180°, 270°)
|
||||
"--clean", # Bild verbessern
|
||||
"--skip-text", # Seiten mit Text überspringen
|
||||
"--force-ocr", # OCR erzwingen falls nötig
|
||||
str(pfad),
|
||||
str(temp_pfad)
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120 # 2 Minuten Timeout
|
||||
timeout=180 # 3 Minuten Timeout (Rotation braucht etwas länger)
|
||||
)
|
||||
|
||||
if result.returncode == 0 and temp_pfad.exists():
|
||||
|
|
|
|||
|
|
@ -52,16 +52,29 @@ class Sorter:
|
|||
original_name = dokument_info.get("original_name", "").lower()
|
||||
absender = dokument_info.get("absender", "").lower()
|
||||
|
||||
# Option: Auch Dateinamen prüfen (Standard: nur Text)
|
||||
auch_dateiname = muster.get("auch_dateiname", False)
|
||||
|
||||
# keywords (einfache Komma-getrennte Liste - für UI)
|
||||
# Verwendet Wortgrenzen (\b) um Teilstring-Matches zu vermeiden
|
||||
# z.B. "rechnung" matched nicht "Versicherungsnummer" oder "Berechnung"
|
||||
if "keywords" in muster:
|
||||
keywords = muster["keywords"]
|
||||
if isinstance(keywords, str):
|
||||
keywords = [k.strip() for k in keywords.split(",")]
|
||||
# Alle Keywords müssen vorkommen
|
||||
# Alle Keywords müssen vorkommen (als ganzes Wort)
|
||||
for keyword in keywords:
|
||||
keyword = keyword.lower().strip()
|
||||
if keyword and keyword not in text and keyword not in original_name:
|
||||
return False
|
||||
if keyword:
|
||||
# Regex mit Wortgrenzen für exaktes Wort-Matching
|
||||
pattern = r'\b' + re.escape(keyword) + r'\b'
|
||||
# Standard: nur im Text suchen, mit Option auch im Dateinamen
|
||||
if auch_dateiname:
|
||||
if not re.search(pattern, text) and not re.search(pattern, original_name):
|
||||
return False
|
||||
else:
|
||||
if not re.search(pattern, text):
|
||||
return False
|
||||
|
||||
# absender_contains
|
||||
if "absender_contains" in muster:
|
||||
|
|
@ -105,15 +118,23 @@ class Sorter:
|
|||
|
||||
# ============ NEGATIVE MUSTER (dürfen NICHT vorkommen) ============
|
||||
|
||||
# keywords_nicht (keines darf vorkommen)
|
||||
# keywords_nicht (keines darf vorkommen - als ganzes Wort)
|
||||
if "keywords_nicht" in muster:
|
||||
keywords = muster["keywords_nicht"]
|
||||
if isinstance(keywords, str):
|
||||
keywords = [k.strip() for k in keywords.split(",")]
|
||||
for keyword in keywords:
|
||||
keyword = keyword.lower().strip()
|
||||
if keyword and (keyword in text or keyword in original_name):
|
||||
return False # Verbotenes Keyword gefunden
|
||||
if keyword:
|
||||
# Regex mit Wortgrenzen für exaktes Wort-Matching
|
||||
pattern = r'\b' + re.escape(keyword) + r'\b'
|
||||
# Standard: nur im Text prüfen, mit Option auch im Dateinamen
|
||||
if auch_dateiname:
|
||||
if re.search(pattern, text) or re.search(pattern, original_name):
|
||||
return False # Verbotenes Keyword gefunden
|
||||
else:
|
||||
if re.search(pattern, text):
|
||||
return False # Verbotenes Keyword gefunden
|
||||
|
||||
# text_not_match (keines darf enthalten sein)
|
||||
if "text_not_match" in muster:
|
||||
|
|
@ -217,7 +238,17 @@ class Sorter:
|
|||
# Datum formatieren
|
||||
if "format" in config:
|
||||
try:
|
||||
datum = datetime.strptime(wert.strip(), config["format"])
|
||||
datum_str = wert.strip()
|
||||
# Deutsche Monatsnamen zu englischen konvertieren
|
||||
de_monate = {
|
||||
'Januar': 'January', 'Februar': 'February', 'März': 'March',
|
||||
'April': 'April', 'Mai': 'May', 'Juni': 'June',
|
||||
'Juli': 'July', 'August': 'August', 'September': 'September',
|
||||
'Oktober': 'October', 'November': 'November', 'Dezember': 'December'
|
||||
}
|
||||
for de, en in de_monate.items():
|
||||
datum_str = datum_str.replace(de, en)
|
||||
datum = datetime.strptime(datum_str, config["format"])
|
||||
return datum.strftime("%Y-%m-%d")
|
||||
except:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import asyncio
|
|||
import tempfile
|
||||
import re
|
||||
|
||||
from ..models.database import get_db, Postfach, QuellOrdner, SortierRegel, VerarbeiteteDatei, VerarbeiteteMail, Zeitplan, OrdnerRegel
|
||||
from ..models.database import get_db, Postfach, QuellOrdner, SortierRegel, VerarbeiteteDatei, VerarbeiteteMail, Zeitplan, OrdnerRegel, DbServer, Datenbank, BackupLog
|
||||
from ..modules.mail_fetcher import MailFetcher
|
||||
from ..modules.pdf_processor import PDFProcessor
|
||||
from ..modules.sorter import Sorter
|
||||
|
|
@ -716,7 +716,11 @@ def verarbeite_ordner(id: int, db: Session = Depends(get_db)):
|
|||
return {"fehler": "Ordner existiert nicht", "verarbeitet": []}
|
||||
|
||||
regeln = db.query(SortierRegel).filter(SortierRegel.aktiv == True).order_by(SortierRegel.prioritaet).all()
|
||||
if not regeln:
|
||||
|
||||
# Prüfen ob direkt_verschieben aktiv ist
|
||||
hat_direkt_verschieben = getattr(quell_ordner, 'direkt_verschieben', False)
|
||||
|
||||
if not regeln and not hat_direkt_verschieben:
|
||||
return {"fehler": "Keine Regeln definiert", "verarbeitet": []}
|
||||
|
||||
# Regeln in Dict-Format
|
||||
|
|
@ -833,6 +837,49 @@ def verarbeite_ordner(id: int, db: Session = Depends(get_db)):
|
|||
ergebnis["verarbeitet"].append(datei_info)
|
||||
continue
|
||||
|
||||
# Direkt verschieben ohne Regel?
|
||||
if hat_direkt_verschieben:
|
||||
# Einfach in Zielordner verschieben ohne Umbenennung
|
||||
ziel_basis.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Original sichern falls konfiguriert
|
||||
original_sichern = getattr(quell_ordner, 'original_sichern', None)
|
||||
if original_sichern:
|
||||
import shutil
|
||||
backup_dir = Path(original_sichern)
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
backup_pfad = backup_dir / datei.name
|
||||
counter = 1
|
||||
while backup_pfad.exists():
|
||||
backup_pfad = backup_dir / f"{datei.stem}_{counter}{datei.suffix}"
|
||||
counter += 1
|
||||
shutil.copy2(str(datei), str(backup_pfad))
|
||||
datei_info["original_gesichert"] = str(backup_pfad)
|
||||
|
||||
neuer_pfad = ziel_basis / datei.name
|
||||
counter = 1
|
||||
while neuer_pfad.exists():
|
||||
neuer_pfad = ziel_basis / f"{datei.stem}_{counter}{datei.suffix}"
|
||||
counter += 1
|
||||
|
||||
datei.rename(neuer_pfad)
|
||||
|
||||
ergebnis["sortiert"] += 1
|
||||
datei_info["neuer_name"] = neuer_pfad.name
|
||||
datei_info["status"] = "direkt_verschoben"
|
||||
|
||||
db.add(VerarbeiteteDatei(
|
||||
original_pfad=str(datei),
|
||||
original_name=datei.name,
|
||||
neuer_pfad=str(neuer_pfad),
|
||||
neuer_name=neuer_pfad.name,
|
||||
ist_zugferd=ist_zugferd,
|
||||
ocr_durchgefuehrt=ocr_gemacht,
|
||||
status="direkt_verschoben"
|
||||
))
|
||||
ergebnis["verarbeitet"].append(datei_info)
|
||||
continue
|
||||
|
||||
# Regel finden
|
||||
doc_info = {
|
||||
"text": text,
|
||||
|
|
@ -1007,6 +1054,95 @@ def teste_regel(data: RegelTestRequest):
|
|||
return {"passt": False}
|
||||
|
||||
|
||||
@router.get("/regeln/export")
|
||||
def export_regeln(db: Session = Depends(get_db)):
|
||||
"""Exportiert alle Regeln als JSON"""
|
||||
regeln = db.query(SortierRegel).order_by(SortierRegel.prioritaet).all()
|
||||
export_data = []
|
||||
for r in regeln:
|
||||
export_data.append({
|
||||
"name": r.name,
|
||||
"prioritaet": r.prioritaet,
|
||||
"muster": r.muster,
|
||||
"extraktion": r.extraktion,
|
||||
"schema": r.schema,
|
||||
"unterordner": r.unterordner,
|
||||
"ziel_ordner": r.ziel_ordner,
|
||||
"nur_umbenennen": r.nur_umbenennen,
|
||||
"ist_fallback": r.ist_fallback,
|
||||
"aktiv": r.aktiv
|
||||
})
|
||||
return {"regeln": export_data, "anzahl": len(export_data)}
|
||||
|
||||
|
||||
class RegelnImportRequest(BaseModel):
|
||||
regeln: List[dict]
|
||||
modus: str = "hinzufuegen" # "hinzufuegen", "ersetzen", "aktualisieren"
|
||||
|
||||
|
||||
@router.post("/regeln/import")
|
||||
def import_regeln(data: RegelnImportRequest, db: Session = Depends(get_db)):
|
||||
"""Importiert Regeln aus JSON"""
|
||||
importiert = 0
|
||||
aktualisiert = 0
|
||||
uebersprungen = 0
|
||||
|
||||
if data.modus == "ersetzen":
|
||||
# Alle bestehenden Regeln löschen
|
||||
db.query(OrdnerRegel).delete()
|
||||
db.query(SortierRegel).delete()
|
||||
|
||||
for regel_data in data.regeln:
|
||||
name = regel_data.get("name")
|
||||
if not name:
|
||||
uebersprungen += 1
|
||||
continue
|
||||
|
||||
# Prüfen ob Regel mit diesem Namen existiert
|
||||
existiert = db.query(SortierRegel).filter(SortierRegel.name == name).first()
|
||||
|
||||
if existiert and data.modus == "hinzufuegen":
|
||||
uebersprungen += 1
|
||||
continue
|
||||
|
||||
if existiert and data.modus == "aktualisieren":
|
||||
# Bestehende Regel aktualisieren
|
||||
existiert.prioritaet = regel_data.get("prioritaet", 100)
|
||||
existiert.muster = regel_data.get("muster", {})
|
||||
existiert.extraktion = regel_data.get("extraktion", {})
|
||||
existiert.schema = regel_data.get("schema", "{datum} - Dokument.pdf")
|
||||
existiert.unterordner = regel_data.get("unterordner")
|
||||
existiert.ziel_ordner = regel_data.get("ziel_ordner")
|
||||
existiert.nur_umbenennen = regel_data.get("nur_umbenennen", False)
|
||||
existiert.ist_fallback = regel_data.get("ist_fallback", False)
|
||||
existiert.aktiv = regel_data.get("aktiv", True)
|
||||
aktualisiert += 1
|
||||
else:
|
||||
# Neue Regel erstellen
|
||||
neue_regel = SortierRegel(
|
||||
name=name,
|
||||
prioritaet=regel_data.get("prioritaet", 100),
|
||||
muster=regel_data.get("muster", {}),
|
||||
extraktion=regel_data.get("extraktion", {}),
|
||||
schema=regel_data.get("schema", "{datum} - Dokument.pdf"),
|
||||
unterordner=regel_data.get("unterordner"),
|
||||
ziel_ordner=regel_data.get("ziel_ordner"),
|
||||
nur_umbenennen=regel_data.get("nur_umbenennen", False),
|
||||
ist_fallback=regel_data.get("ist_fallback", False),
|
||||
aktiv=regel_data.get("aktiv", True)
|
||||
)
|
||||
db.add(neue_regel)
|
||||
importiert += 1
|
||||
|
||||
db.commit()
|
||||
return {
|
||||
"importiert": importiert,
|
||||
"aktualisiert": aktualisiert,
|
||||
"uebersprungen": uebersprungen,
|
||||
"gesamt": len(data.regeln)
|
||||
}
|
||||
|
||||
|
||||
# ============ Ordner-Regel-Zuweisungen ============
|
||||
|
||||
@router.get("/ordner/{ordner_id}/regeln")
|
||||
|
|
@ -1507,6 +1643,207 @@ def starte_sortierung(db: Session = Depends(get_db)):
|
|||
return ergebnis
|
||||
|
||||
|
||||
@router.get("/sortierung/starten/stream")
|
||||
async def starte_sortierung_stream(db: Session = Depends(get_db)):
|
||||
"""Streaming-Endpoint für Feinsortierung mit Live-Updates"""
|
||||
from ..models.database import SessionLocal
|
||||
|
||||
# Daten vorab laden (Session ist im Generator nicht verfügbar)
|
||||
ordner_liste = db.query(QuellOrdner).filter(QuellOrdner.aktiv == True).all()
|
||||
ordner_daten = [{
|
||||
"id": o.id, "name": o.name, "ziel_ordner": o.ziel_ordner,
|
||||
"dateitypen": o.dateitypen, "ocr_aktivieren": getattr(o, 'ocr_aktivieren', True),
|
||||
"original_sichern": getattr(o, 'original_sichern', None),
|
||||
"signiert_behandlung": getattr(o, 'signiert_behandlung', 'normal')
|
||||
} for o in ordner_liste]
|
||||
|
||||
# Fallback-Regeln laden
|
||||
fallback_regeln = db.query(SortierRegel).filter(
|
||||
SortierRegel.aktiv == True,
|
||||
SortierRegel.ist_fallback == True
|
||||
).order_by(SortierRegel.prioritaet).all()
|
||||
fallback_dicts = [{
|
||||
"id": r.id, "name": r.name, "prioritaet": r.prioritaet,
|
||||
"muster": r.muster, "extraktion": r.extraktion,
|
||||
"schema": r.schema, "unterordner": r.unterordner,
|
||||
"ziel_ordner": getattr(r, 'ziel_ordner', None),
|
||||
"nur_umbenennen": getattr(r, 'nur_umbenennen', False)
|
||||
} for r in fallback_regeln]
|
||||
|
||||
# Ordner-Regeln vorladen
|
||||
ordner_regeln_map = {}
|
||||
for o in ordner_daten:
|
||||
zuweisungen = db.query(OrdnerRegel).filter(OrdnerRegel.ordner_id == o["id"]).all()
|
||||
zugewiesene_ids = [z.regel_id for z in zuweisungen]
|
||||
if zugewiesene_ids:
|
||||
regeln = db.query(SortierRegel).filter(
|
||||
SortierRegel.id.in_(zugewiesene_ids),
|
||||
SortierRegel.aktiv == True,
|
||||
SortierRegel.ist_fallback == False
|
||||
).order_by(SortierRegel.prioritaet).all()
|
||||
ordner_regeln_map[o["id"]] = [{
|
||||
"id": r.id, "name": r.name, "prioritaet": r.prioritaet,
|
||||
"muster": r.muster, "extraktion": r.extraktion,
|
||||
"schema": r.schema, "unterordner": r.unterordner,
|
||||
"ziel_ordner": getattr(r, 'ziel_ordner', None),
|
||||
"nur_umbenennen": getattr(r, 'nur_umbenennen', False)
|
||||
} for r in regeln]
|
||||
else:
|
||||
ordner_regeln_map[o["id"]] = []
|
||||
|
||||
# Dateien vorab zählen für Gesamtübersicht
|
||||
gesamt_dateien = 0
|
||||
for o in ordner_daten:
|
||||
pfad = Path(o["ziel_ordner"])
|
||||
if pfad.exists():
|
||||
dateien = sammle_dateien_aus_pfad(str(pfad), [".pdf"], rekursiv=True)
|
||||
gesamt_dateien += len(dateien)
|
||||
|
||||
async def event_generator():
|
||||
def send_event(data):
|
||||
return f"data: {json.dumps(data)}\n\n"
|
||||
|
||||
if not ordner_daten:
|
||||
yield send_event({"type": "fehler", "nachricht": "Keine Quell-Ordner konfiguriert"})
|
||||
return
|
||||
|
||||
pdf_processor = PDFProcessor()
|
||||
ergebnis = {"gesamt": 0, "sortiert": 0, "zugferd": 0, "fehler": 0}
|
||||
|
||||
yield send_event({"type": "start", "ordner_count": len(ordner_daten), "gesamt": gesamt_dateien})
|
||||
await asyncio.sleep(0) # Sofort senden
|
||||
|
||||
session = SessionLocal()
|
||||
try:
|
||||
for quell_ordner in ordner_daten:
|
||||
pfad = Path(quell_ordner["ziel_ordner"])
|
||||
if not pfad.exists():
|
||||
continue
|
||||
|
||||
dateien = sammle_dateien_aus_pfad(str(pfad), [".pdf"], rekursiv=True)
|
||||
regeln_dicts = ordner_regeln_map.get(quell_ordner["id"], [])
|
||||
|
||||
yield send_event({
|
||||
"type": "ordner",
|
||||
"ordner": quell_ordner["name"],
|
||||
"dateien": len(dateien),
|
||||
"regeln": len(regeln_dicts)
|
||||
})
|
||||
await asyncio.sleep(0) # Sofort senden
|
||||
|
||||
sorter = Sorter(regeln_dicts) if regeln_dicts else None
|
||||
fallback_sorter = Sorter(fallback_dicts) if fallback_dicts else None
|
||||
|
||||
for datei in dateien:
|
||||
ergebnis["gesamt"] += 1
|
||||
datei_info = {"original": datei.name, "ordner": quell_ordner["name"]}
|
||||
|
||||
yield send_event({"type": "datei_start", "datei": datei.name})
|
||||
await asyncio.sleep(0) # Sofort senden
|
||||
|
||||
try:
|
||||
ist_pdf = datei.suffix.lower() == ".pdf"
|
||||
text = ""
|
||||
ocr_gemacht = False
|
||||
|
||||
if ist_pdf:
|
||||
pdf_result = pdf_processor.verarbeite(
|
||||
str(datei),
|
||||
ocr_erlaubt=quell_ordner.get("ocr_aktivieren", True),
|
||||
original_backup_pfad=quell_ordner.get("original_sichern")
|
||||
)
|
||||
if pdf_result.get("fehler"):
|
||||
raise Exception(pdf_result["fehler"])
|
||||
text = pdf_result.get("text", "")
|
||||
ocr_gemacht = pdf_result.get("ocr_durchgefuehrt", False)
|
||||
|
||||
doc_info = {
|
||||
"text": text,
|
||||
"original_name": datei.name,
|
||||
"absender": "",
|
||||
"dateityp": datei.suffix.lower()
|
||||
}
|
||||
|
||||
# Passende Regel finden
|
||||
regel = None
|
||||
if sorter:
|
||||
regel = sorter.finde_passende_regel(doc_info)
|
||||
if not regel and fallback_sorter:
|
||||
regel = fallback_sorter.finde_passende_regel(doc_info)
|
||||
if regel:
|
||||
datei_info["fallback"] = True
|
||||
|
||||
if not regel:
|
||||
datei_info["fehler"] = "Keine passende Regel"
|
||||
ergebnis["fehler"] += 1
|
||||
yield send_event({"type": "datei_fehler", **datei_info})
|
||||
await asyncio.sleep(0)
|
||||
continue
|
||||
|
||||
# Felder extrahieren und umbenennen
|
||||
extrahiert = (sorter or fallback_sorter).extrahiere_felder(regel, doc_info)
|
||||
schema = regel.get("schema", "{datum} - Dokument.pdf")
|
||||
if schema.endswith(".pdf"):
|
||||
schema = schema[:-4] + datei.suffix
|
||||
neuer_name = (sorter or fallback_sorter).generiere_dateinamen(
|
||||
{"schema": schema, **regel}, extrahiert
|
||||
)
|
||||
|
||||
# Zielordner
|
||||
if regel.get("nur_umbenennen"):
|
||||
ziel = datei.parent
|
||||
elif regel.get("ziel_ordner"):
|
||||
ziel = Path(regel["ziel_ordner"])
|
||||
if regel.get("unterordner"):
|
||||
ziel = ziel / regel["unterordner"]
|
||||
else:
|
||||
ziel = pfad
|
||||
if regel.get("unterordner"):
|
||||
ziel = ziel / regel["unterordner"]
|
||||
ziel.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
neuer_pfad = (sorter or fallback_sorter).verschiebe_datei(str(datei), str(ziel), neuer_name)
|
||||
|
||||
ergebnis["sortiert"] += 1
|
||||
datei_info["neuer_name"] = neuer_name
|
||||
datei_info["regel"] = regel.get("name", "Unbekannt")
|
||||
|
||||
session.add(VerarbeiteteDatei(
|
||||
original_pfad=str(datei),
|
||||
original_name=datei.name,
|
||||
neuer_pfad=neuer_pfad,
|
||||
neuer_name=neuer_name,
|
||||
ocr_durchgefuehrt=ocr_gemacht,
|
||||
status="sortiert",
|
||||
extrahierte_daten=extrahiert
|
||||
))
|
||||
|
||||
yield send_event({"type": "datei_fertig", **datei_info})
|
||||
await asyncio.sleep(0) # Sofort senden
|
||||
|
||||
except Exception as e:
|
||||
ergebnis["fehler"] += 1
|
||||
datei_info["fehler"] = str(e)
|
||||
yield send_event({"type": "datei_fehler", **datei_info})
|
||||
await asyncio.sleep(0)
|
||||
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
yield send_event({"type": "fertig", **ergebnis})
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============ PDF Test / Regel-Vorschau ============
|
||||
|
||||
@router.post("/pdf/extrahieren")
|
||||
|
|
@ -2172,3 +2509,276 @@ def status_uebersicht(db: Session = Depends(get_db)):
|
|||
"quell_ordner": ordner_status,
|
||||
"scheduler": scheduler_status
|
||||
}
|
||||
|
||||
|
||||
# ============ DB-Server API ============
|
||||
|
||||
class DbServerCreate(BaseModel):
|
||||
name: str
|
||||
typ: str # mariadb, mysql, postgresql
|
||||
host: str
|
||||
port: int = 3306
|
||||
user: str
|
||||
password: str
|
||||
|
||||
|
||||
class DbServerResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
typ: str
|
||||
host: str
|
||||
port: int
|
||||
user: str
|
||||
aktiv: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/dbserver")
|
||||
def liste_dbserver(db: Session = Depends(get_db)):
|
||||
"""Liste aller DB-Server"""
|
||||
return db.query(DbServer).all()
|
||||
|
||||
|
||||
@router.get("/dbserver/{id}")
|
||||
def hole_dbserver(id: int, db: Session = Depends(get_db)):
|
||||
"""Einzelnen DB-Server abrufen"""
|
||||
server = db.query(DbServer).filter(DbServer.id == id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||
return {
|
||||
"id": server.id,
|
||||
"name": server.name,
|
||||
"typ": server.typ,
|
||||
"host": server.host,
|
||||
"port": server.port,
|
||||
"user": server.user,
|
||||
"aktiv": server.aktiv
|
||||
}
|
||||
|
||||
|
||||
@router.post("/dbserver")
|
||||
def erstelle_dbserver(data: DbServerCreate, db: Session = Depends(get_db)):
|
||||
"""Neuen DB-Server erstellen"""
|
||||
server = DbServer(
|
||||
name=data.name,
|
||||
typ=data.typ,
|
||||
host=data.host,
|
||||
port=data.port,
|
||||
user=data.user,
|
||||
passwort=data.password
|
||||
)
|
||||
db.add(server)
|
||||
db.commit()
|
||||
db.refresh(server)
|
||||
return {"id": server.id, "message": "Erstellt"}
|
||||
|
||||
|
||||
@router.put("/dbserver/{id}")
|
||||
def aktualisiere_dbserver(id: int, data: DbServerCreate, db: Session = Depends(get_db)):
|
||||
"""DB-Server aktualisieren"""
|
||||
server = db.query(DbServer).filter(DbServer.id == id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||
|
||||
server.name = data.name
|
||||
server.typ = data.typ
|
||||
server.host = data.host
|
||||
server.port = data.port
|
||||
server.user = data.user
|
||||
if data.password:
|
||||
server.passwort = data.password
|
||||
|
||||
db.commit()
|
||||
return {"message": "Aktualisiert"}
|
||||
|
||||
|
||||
@router.delete("/dbserver/{id}")
|
||||
def loesche_dbserver(id: int, db: Session = Depends(get_db)):
|
||||
"""DB-Server löschen"""
|
||||
server = db.query(DbServer).filter(DbServer.id == id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||
db.delete(server)
|
||||
db.commit()
|
||||
return {"message": "Gelöscht"}
|
||||
|
||||
|
||||
@router.post("/dbserver/{id}/aktivieren")
|
||||
def aktiviere_dbserver(id: int, db: Session = Depends(get_db)):
|
||||
"""DB-Server aktivieren/deaktivieren"""
|
||||
server = db.query(DbServer).filter(DbServer.id == id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||
server.aktiv = not server.aktiv
|
||||
db.commit()
|
||||
return {"aktiv": server.aktiv}
|
||||
|
||||
|
||||
class DbServerTest(BaseModel):
|
||||
typ: str
|
||||
host: str
|
||||
port: int = 3306
|
||||
user: str
|
||||
password: str
|
||||
|
||||
|
||||
@router.post("/dbserver/test")
|
||||
def teste_dbserver(data: DbServerTest):
|
||||
"""Testet die Verbindung zu einem DB-Server"""
|
||||
try:
|
||||
if data.typ in ["mariadb", "mysql"]:
|
||||
import pymysql
|
||||
conn = pymysql.connect(
|
||||
host=data.host,
|
||||
port=data.port,
|
||||
user=data.user,
|
||||
password=data.password,
|
||||
connect_timeout=5
|
||||
)
|
||||
conn.close()
|
||||
elif data.typ == "postgresql":
|
||||
import psycopg2
|
||||
conn = psycopg2.connect(
|
||||
host=data.host,
|
||||
port=data.port,
|
||||
user=data.user,
|
||||
password=data.password,
|
||||
connect_timeout=5
|
||||
)
|
||||
conn.close()
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Unbekannter DB-Typ")
|
||||
|
||||
return {"message": "Verbindung erfolgreich!"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
# ============ Datenbanken API ============
|
||||
|
||||
class DatenbankCreate(BaseModel):
|
||||
name: str
|
||||
server_id: int
|
||||
database: str
|
||||
backup_pfad: str
|
||||
aufbewahrung: int = 7
|
||||
format: str = "sql" # sql, sql.gz, zip
|
||||
|
||||
|
||||
class DatenbankResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
server_id: int
|
||||
database: str
|
||||
backup_pfad: str
|
||||
aufbewahrung: int
|
||||
format: str
|
||||
aktiv: bool
|
||||
letztes_backup: Optional[datetime]
|
||||
server_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/datenbanken")
|
||||
def liste_datenbanken(db: Session = Depends(get_db)):
|
||||
"""Liste aller Datenbanken mit Server-Namen"""
|
||||
dbs = db.query(Datenbank).all()
|
||||
result = []
|
||||
for d in dbs:
|
||||
server = db.query(DbServer).filter(DbServer.id == d.server_id).first()
|
||||
result.append({
|
||||
"id": d.id,
|
||||
"name": d.name,
|
||||
"server_id": d.server_id,
|
||||
"server_name": server.name if server else None,
|
||||
"database": d.database,
|
||||
"backup_pfad": d.backup_pfad,
|
||||
"aufbewahrung": d.aufbewahrung,
|
||||
"format": d.format,
|
||||
"aktiv": d.aktiv,
|
||||
"letztes_backup": d.letztes_backup
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/datenbanken/{id}")
|
||||
def hole_datenbank(id: int, db: Session = Depends(get_db)):
|
||||
"""Einzelne Datenbank abrufen"""
|
||||
datenbank = db.query(Datenbank).filter(Datenbank.id == id).first()
|
||||
if not datenbank:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||
return datenbank
|
||||
|
||||
|
||||
@router.post("/datenbanken")
|
||||
def erstelle_datenbank(data: DatenbankCreate, db: Session = Depends(get_db)):
|
||||
"""Neue Datenbank-Konfiguration erstellen"""
|
||||
datenbank = Datenbank(**data.dict())
|
||||
db.add(datenbank)
|
||||
db.commit()
|
||||
db.refresh(datenbank)
|
||||
return {"id": datenbank.id, "message": "Erstellt"}
|
||||
|
||||
|
||||
@router.put("/datenbanken/{id}")
|
||||
def aktualisiere_datenbank(id: int, data: DatenbankCreate, db: Session = Depends(get_db)):
|
||||
"""Datenbank-Konfiguration aktualisieren"""
|
||||
datenbank = db.query(Datenbank).filter(Datenbank.id == id).first()
|
||||
if not datenbank:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||
|
||||
for key, value in data.dict().items():
|
||||
setattr(datenbank, key, value)
|
||||
db.commit()
|
||||
return {"message": "Aktualisiert"}
|
||||
|
||||
|
||||
@router.delete("/datenbanken/{id}")
|
||||
def loesche_datenbank(id: int, db: Session = Depends(get_db)):
|
||||
"""Datenbank-Konfiguration löschen"""
|
||||
datenbank = db.query(Datenbank).filter(Datenbank.id == id).first()
|
||||
if not datenbank:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||
db.delete(datenbank)
|
||||
db.commit()
|
||||
return {"message": "Gelöscht"}
|
||||
|
||||
|
||||
@router.post("/datenbanken/{id}/aktivieren")
|
||||
def aktiviere_datenbank(id: int, db: Session = Depends(get_db)):
|
||||
"""Datenbank aktivieren/deaktivieren"""
|
||||
datenbank = db.query(Datenbank).filter(Datenbank.id == id).first()
|
||||
if not datenbank:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||
datenbank.aktiv = not datenbank.aktiv
|
||||
db.commit()
|
||||
return {"aktiv": datenbank.aktiv}
|
||||
|
||||
|
||||
@router.post("/datenbanken/{id}/backup")
|
||||
def erstelle_backup(id: int, db: Session = Depends(get_db)):
|
||||
"""Erstellt ein Backup der angegebenen Datenbank"""
|
||||
from ..services.backup_service import erstelle_db_backup
|
||||
return erstelle_db_backup(id, db)
|
||||
|
||||
|
||||
@router.get("/backups")
|
||||
def liste_backups(db: Session = Depends(get_db)):
|
||||
"""Liste der letzten Backups"""
|
||||
logs = db.query(BackupLog).order_by(BackupLog.erstellt_am.desc()).limit(50).all()
|
||||
result = []
|
||||
for log in logs:
|
||||
datenbank = db.query(Datenbank).filter(Datenbank.id == log.datenbank_id).first()
|
||||
result.append({
|
||||
"id": log.id,
|
||||
"datenbank_name": datenbank.name if datenbank else "Unbekannt",
|
||||
"dateiname": log.dateiname,
|
||||
"groesse_mb": round(log.groesse_bytes / 1024 / 1024, 2) if log.groesse_bytes else 0,
|
||||
"status": log.status,
|
||||
"erstellt": log.erstellt_am.strftime("%d.%m.%Y %H:%M") if log.erstellt_am else None
|
||||
})
|
||||
return result
|
||||
|
|
|
|||
262
Source/backend/app/services/backup_service.py
Executable file
262
Source/backend/app/services/backup_service.py
Executable file
|
|
@ -0,0 +1,262 @@
|
|||
"""
|
||||
Datenbank-Backup Service
|
||||
Unterstützt MariaDB, MySQL und PostgreSQL
|
||||
"""
|
||||
import subprocess
|
||||
import os
|
||||
import gzip
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
import logging
|
||||
|
||||
from ..models.database import DbServer, Datenbank, BackupLog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def erstelle_db_backup(datenbank_id: int, db: Session) -> dict:
|
||||
"""
|
||||
Erstellt ein Backup einer Datenbank.
|
||||
|
||||
Args:
|
||||
datenbank_id: ID der Datenbank-Konfiguration
|
||||
db: SQLAlchemy Session
|
||||
|
||||
Returns:
|
||||
dict mit Backup-Informationen
|
||||
"""
|
||||
# Datenbank-Konfiguration laden
|
||||
datenbank = db.query(Datenbank).filter(Datenbank.id == datenbank_id).first()
|
||||
if not datenbank:
|
||||
raise Exception("Datenbank nicht gefunden")
|
||||
|
||||
# Server-Konfiguration laden
|
||||
server = db.query(DbServer).filter(DbServer.id == datenbank.server_id).first()
|
||||
if not server:
|
||||
raise Exception("DB-Server nicht gefunden")
|
||||
|
||||
# Backup-Verzeichnis erstellen
|
||||
backup_dir = Path(datenbank.backup_pfad)
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Dateiname generieren
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
base_name = f"{datenbank.database}_{timestamp}"
|
||||
|
||||
try:
|
||||
# Backup erstellen basierend auf DB-Typ
|
||||
if server.typ in ["mariadb", "mysql"]:
|
||||
sql_file = backup_mysql(server, datenbank.database, backup_dir, base_name)
|
||||
elif server.typ == "postgresql":
|
||||
sql_file = backup_postgresql(server, datenbank.database, backup_dir, base_name)
|
||||
else:
|
||||
raise Exception(f"Unbekannter Datenbanktyp: {server.typ}")
|
||||
|
||||
# Format anwenden (Komprimierung)
|
||||
final_file = sql_file
|
||||
if datenbank.format == "sql.gz":
|
||||
final_file = komprimiere_gzip(sql_file)
|
||||
os.remove(sql_file) # Original löschen
|
||||
elif datenbank.format == "zip":
|
||||
final_file = komprimiere_zip(sql_file)
|
||||
os.remove(sql_file) # Original löschen
|
||||
|
||||
# Dateigröße ermitteln
|
||||
file_size = os.path.getsize(final_file)
|
||||
|
||||
# Backup in DB loggen
|
||||
log_entry = BackupLog(
|
||||
datenbank_id=datenbank.id,
|
||||
dateiname=os.path.basename(final_file),
|
||||
groesse_bytes=file_size,
|
||||
status="erfolg"
|
||||
)
|
||||
db.add(log_entry)
|
||||
|
||||
# Datenbank-Status aktualisieren
|
||||
datenbank.letztes_backup = datetime.now()
|
||||
datenbank.letzte_groesse_mb = round(file_size / 1024 / 1024)
|
||||
db.commit()
|
||||
|
||||
# Alte Backups aufräumen
|
||||
bereinige_alte_backups(datenbank, db)
|
||||
|
||||
logger.info(f"Backup erstellt: {final_file} ({file_size} bytes)")
|
||||
print(f"[BACKUP] ✓ Backup erstellt: {final_file}", flush=True)
|
||||
|
||||
return {
|
||||
"erfolg": True,
|
||||
"datei": os.path.basename(final_file),
|
||||
"groesse_mb": round(file_size / 1024 / 1024, 2),
|
||||
"pfad": str(final_file)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Fehler loggen
|
||||
log_entry = BackupLog(
|
||||
datenbank_id=datenbank.id,
|
||||
dateiname=f"{base_name}_FEHLER",
|
||||
status="fehler",
|
||||
fehler=str(e)
|
||||
)
|
||||
db.add(log_entry)
|
||||
db.commit()
|
||||
|
||||
logger.error(f"Backup-Fehler: {e}")
|
||||
print(f"[BACKUP] ✗ Fehler: {e}", flush=True)
|
||||
raise Exception(f"Backup fehlgeschlagen: {e}")
|
||||
|
||||
|
||||
def backup_mysql(server: DbServer, database: str, backup_dir: Path, base_name: str) -> str:
|
||||
"""
|
||||
Erstellt ein MySQL/MariaDB Backup mit mysqldump.
|
||||
"""
|
||||
output_file = backup_dir / f"{base_name}.sql"
|
||||
|
||||
cmd = [
|
||||
"mysqldump",
|
||||
f"--host={server.host}",
|
||||
f"--port={server.port}",
|
||||
f"--user={server.user}",
|
||||
f"--password={server.passwort}",
|
||||
"--single-transaction",
|
||||
"--routines",
|
||||
"--triggers",
|
||||
"--events",
|
||||
database
|
||||
]
|
||||
|
||||
print(f"[BACKUP] Starte MySQL-Dump für {database}...", flush=True)
|
||||
|
||||
with open(output_file, 'w') as f:
|
||||
result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, timeout=3600)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr.decode('utf-8', errors='ignore')
|
||||
raise Exception(f"mysqldump Fehler: {error_msg}")
|
||||
|
||||
return str(output_file)
|
||||
|
||||
|
||||
def backup_postgresql(server: DbServer, database: str, backup_dir: Path, base_name: str) -> str:
|
||||
"""
|
||||
Erstellt ein PostgreSQL Backup mit pg_dump.
|
||||
"""
|
||||
output_file = backup_dir / f"{base_name}.sql"
|
||||
|
||||
# Passwort über Umgebungsvariable
|
||||
env = os.environ.copy()
|
||||
env["PGPASSWORD"] = server.passwort
|
||||
|
||||
cmd = [
|
||||
"pg_dump",
|
||||
f"--host={server.host}",
|
||||
f"--port={server.port}",
|
||||
f"--username={server.user}",
|
||||
"--format=plain",
|
||||
"--no-owner",
|
||||
"--no-acl",
|
||||
database
|
||||
]
|
||||
|
||||
print(f"[BACKUP] Starte PostgreSQL-Dump für {database}...", flush=True)
|
||||
|
||||
with open(output_file, 'w') as f:
|
||||
result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, env=env, timeout=3600)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr.decode('utf-8', errors='ignore')
|
||||
raise Exception(f"pg_dump Fehler: {error_msg}")
|
||||
|
||||
return str(output_file)
|
||||
|
||||
|
||||
def komprimiere_gzip(input_file: str) -> str:
|
||||
"""
|
||||
Komprimiert eine Datei mit gzip.
|
||||
"""
|
||||
output_file = f"{input_file}.gz"
|
||||
|
||||
with open(input_file, 'rb') as f_in:
|
||||
with gzip.open(output_file, 'wb') as f_out:
|
||||
f_out.writelines(f_in)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def komprimiere_zip(input_file: str) -> str:
|
||||
"""
|
||||
Komprimiert eine Datei als ZIP.
|
||||
"""
|
||||
output_file = input_file.replace('.sql', '.zip')
|
||||
|
||||
with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.write(input_file, os.path.basename(input_file))
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def bereinige_alte_backups(datenbank: Datenbank, db: Session):
|
||||
"""
|
||||
Löscht alte Backups basierend auf der Aufbewahrungsregel.
|
||||
"""
|
||||
backup_dir = Path(datenbank.backup_pfad)
|
||||
if not backup_dir.exists():
|
||||
return
|
||||
|
||||
# Alle Backup-Dateien für diese Datenbank finden
|
||||
pattern = f"{datenbank.database}_*"
|
||||
backups = sorted(backup_dir.glob(pattern), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
|
||||
# Nur die neuesten behalten
|
||||
zu_loeschen = backups[datenbank.aufbewahrung:]
|
||||
|
||||
for backup_file in zu_loeschen:
|
||||
try:
|
||||
backup_file.unlink()
|
||||
print(f"[BACKUP] Altes Backup gelöscht: {backup_file.name}", flush=True)
|
||||
|
||||
# Auch aus der DB löschen
|
||||
db.query(BackupLog).filter(BackupLog.dateiname == backup_file.name).delete()
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte altes Backup nicht löschen: {e}")
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
def fuehre_alle_backups_aus(db: Session) -> dict:
|
||||
"""
|
||||
Führt Backups für alle aktiven Datenbanken aus.
|
||||
|
||||
Returns:
|
||||
dict mit Zusammenfassung
|
||||
"""
|
||||
datenbanken = db.query(Datenbank).filter(Datenbank.aktiv == True).all()
|
||||
|
||||
ergebnis = {
|
||||
"gesamt": len(datenbanken),
|
||||
"erfolgreich": 0,
|
||||
"fehlgeschlagen": 0,
|
||||
"details": []
|
||||
}
|
||||
|
||||
for datenbank in datenbanken:
|
||||
try:
|
||||
result = erstelle_db_backup(datenbank.id, db)
|
||||
ergebnis["erfolgreich"] += 1
|
||||
ergebnis["details"].append({
|
||||
"name": datenbank.name,
|
||||
"status": "erfolg",
|
||||
"datei": result.get("datei")
|
||||
})
|
||||
except Exception as e:
|
||||
ergebnis["fehlgeschlagen"] += 1
|
||||
ergebnis["details"].append({
|
||||
"name": datenbank.name,
|
||||
"status": "fehler",
|
||||
"fehler": str(e)
|
||||
})
|
||||
|
||||
return ergebnis
|
||||
|
|
@ -361,6 +361,8 @@ def execute_zeitplan(zeitplan_id: int):
|
|||
elif zeitplan.typ == "sortierung":
|
||||
# Legacy: alte "sortierung" wird wie "grobsortierung" behandelt
|
||||
result = execute_grobsortierung(db, zeitplan)
|
||||
elif zeitplan.typ == "db_backup":
|
||||
result = execute_db_backup(db, zeitplan)
|
||||
else:
|
||||
result = {"erfolg": False, "meldung": f"Unbekannter Typ: {zeitplan.typ}"}
|
||||
|
||||
|
|
@ -511,10 +513,16 @@ def execute_grobsortierung(db, zeitplan: Zeitplan) -> Dict:
|
|||
regeln = db.query(SortierRegel).filter(SortierRegel.aktiv == True).order_by(SortierRegel.prioritaet).all()
|
||||
print(f"[GROBSORTIERUNG] Gefunden: {len(regeln)} aktive Regeln", flush=True)
|
||||
|
||||
if not regeln:
|
||||
print("[GROBSORTIERUNG] ⚠️ Keine aktiven Regeln - Abbruch", flush=True)
|
||||
# Prüfen ob mindestens ein Ordner direkt_verschieben aktiviert hat
|
||||
hat_direkt_verschieben = any(getattr(qo, 'direkt_verschieben', False) for qo in quell_ordner)
|
||||
|
||||
if not regeln and not hat_direkt_verschieben:
|
||||
print("[GROBSORTIERUNG] ⚠️ Keine aktiven Regeln und kein direkt_verschieben - Abbruch", flush=True)
|
||||
return {"erfolg": False, "meldung": "Keine aktiven Regeln definiert"}
|
||||
|
||||
if not regeln:
|
||||
print("[GROBSORTIERUNG] ℹ️ Keine Regeln, aber direkt_verschieben aktiv - fahre fort", flush=True)
|
||||
|
||||
# Regeln in Dict-Format
|
||||
regeln_dicts = [{
|
||||
"id": r.id,
|
||||
|
|
@ -860,6 +868,48 @@ def execute_sortierregeln(db, zeitplan: Zeitplan) -> Dict:
|
|||
return {"erfolg": gesamt_fehler == 0, "meldung": meldung}
|
||||
|
||||
|
||||
def execute_db_backup(db, zeitplan: Zeitplan) -> Dict:
|
||||
"""Führt Datenbank-Backup aus"""
|
||||
from ..models.database import Datenbank
|
||||
from .backup_service import erstelle_db_backup
|
||||
|
||||
print(f"[SCHEDULER] Starte DB-Backup: {zeitplan.name}", flush=True)
|
||||
|
||||
# Datenbanken laden (optional spezifische Datenbank)
|
||||
if zeitplan.datenbank_id:
|
||||
datenbanken = db.query(Datenbank).filter(
|
||||
Datenbank.id == zeitplan.datenbank_id,
|
||||
Datenbank.aktiv == True
|
||||
).all()
|
||||
else:
|
||||
datenbanken = db.query(Datenbank).filter(Datenbank.aktiv == True).all()
|
||||
|
||||
if not datenbanken:
|
||||
return {"erfolg": True, "meldung": "Keine aktiven Datenbanken gefunden"}
|
||||
|
||||
gesamt_erfolg = 0
|
||||
gesamt_fehler = 0
|
||||
fehler_meldungen = []
|
||||
|
||||
for datenbank in datenbanken:
|
||||
try:
|
||||
print(f"[SCHEDULER] Backup für: {datenbank.name}", flush=True)
|
||||
result = erstelle_db_backup(datenbank.id, db)
|
||||
gesamt_erfolg += 1
|
||||
except Exception as e:
|
||||
gesamt_fehler += 1
|
||||
fehler_meldungen.append(f"{datenbank.name}: {str(e)}")
|
||||
logger.error(f"Backup-Fehler für {datenbank.name}: {e}")
|
||||
|
||||
meldung = f"{gesamt_erfolg} Backups erstellt"
|
||||
if gesamt_fehler > 0:
|
||||
meldung += f", {gesamt_fehler} Fehler"
|
||||
if fehler_meldungen:
|
||||
meldung += f" ({'; '.join(fehler_meldungen[:3])})"
|
||||
|
||||
return {"erfolg": gesamt_fehler == 0, "meldung": meldung}
|
||||
|
||||
|
||||
def get_scheduler_status() -> Dict:
|
||||
"""Gibt den Status aller Zeitpläne zurück"""
|
||||
global scheduler
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ jinja2==3.1.3
|
|||
sqlalchemy==2.0.25
|
||||
aiosqlite==0.19.0
|
||||
pymysql==1.1.0
|
||||
psycopg2-binary==2.9.9
|
||||
|
||||
# PDF Processing
|
||||
pypdf==4.0.1
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ services:
|
|||
- /mnt:/mnt
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- DATABASE_URL=mysql+pymysql://data:8715@192.168.155.83/dateiverwaltung
|
||||
- DATABASE_URL=mysql+pymysql://data:8715@192.168.155.11/dateiverwaltung
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
|
|
|
|||
|
|
@ -50,6 +50,163 @@ body {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============ Tab Navigation ============ */
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 1rem 1.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* ============ Tab Content ============ */
|
||||
.tab-content {
|
||||
display: none;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tab-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 35% 35% 30%;
|
||||
gap: 1px;
|
||||
height: calc(100vh - 120px);
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.tab-left,
|
||||
.tab-center,
|
||||
.tab-right {
|
||||
background: var(--bg);
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tab-left {
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab-right {
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.tab-layout {
|
||||
grid-template-columns: 42% 42% 16%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.tab-layout {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
.tab-left {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
.tab-center {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
.tab-right {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
.tab-right .card {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tab-layout {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
}
|
||||
.tab-left,
|
||||
.tab-center,
|
||||
.tab-right {
|
||||
border: none;
|
||||
}
|
||||
.tab-right {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ Debug Log Header ============ */
|
||||
.header-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.debug-log-header {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.8rem;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.debug-log-label {
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.debug-log-text {
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.debug-log-text.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.debug-log-text.success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.debug-log-text.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
/* ============ Old Main Container (deprecated) ============ */
|
||||
.main-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
|
|
|||
|
|
@ -341,6 +341,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
ladeZeitplaene();
|
||||
ladeStatus();
|
||||
|
||||
// Gespeicherten Tab wiederherstellen
|
||||
const gespeicherterTab = localStorage.getItem('aktiver-tab');
|
||||
if (gespeicherterTab) {
|
||||
wechsleTab(gespeicherterTab);
|
||||
}
|
||||
|
||||
// Event-Listener für Dateityp-Checkboxen im Postfach-Modal
|
||||
const pfTypenGruppe = document.getElementById('pf-typen-gruppe');
|
||||
if (pfTypenGruppe) {
|
||||
|
|
@ -985,26 +991,37 @@ async function ordnerVorschau(id) {
|
|||
async function ordnerVerarbeiten(id) {
|
||||
if (!await showConfirm('Dateien jetzt verarbeiten und sortieren?')) return;
|
||||
|
||||
const logContainer = document.getElementById('grobsortierung-log');
|
||||
logContainer.innerHTML = '<div class="log-entry info">Verarbeite...</div>';
|
||||
|
||||
try {
|
||||
zeigeLoading('Verarbeite Dateien...');
|
||||
debugLog('Verarbeite Ordner...', 'info');
|
||||
const result = await api(`/ordner/${id}/verarbeiten`, { method: 'POST' });
|
||||
|
||||
let msg = `Verarbeitung abgeschlossen:\n\n`;
|
||||
msg += `• Gesamt: ${result.gesamt}\n`;
|
||||
msg += `• Sortiert: ${result.sortiert}\n`;
|
||||
msg += `• ZUGFeRD: ${result.zugferd}\n`;
|
||||
msg += `• Keine Regel: ${result.keine_regel || 0}\n`;
|
||||
msg += `• Fehler: ${result.fehler}`;
|
||||
// Ergebnis in der Mitte anzeigen
|
||||
let html = `<div class="log-entry success">
|
||||
<strong>Verarbeitung abgeschlossen</strong><br>
|
||||
Gesamt: ${result.gesamt} | Sortiert: ${result.sortiert} | ZUGFeRD: ${result.zugferd} | Keine Regel: ${result.keine_regel || 0} | Fehler: ${result.fehler}
|
||||
</div>`;
|
||||
|
||||
if (result.fehler && result.fehler > 0) {
|
||||
showAlert(msg, 'warning', 'Verarbeitung mit Warnungen');
|
||||
} else {
|
||||
showAlert(msg, 'success', 'Verarbeitung abgeschlossen');
|
||||
// Details der verarbeiteten Dateien anzeigen
|
||||
if (result.verarbeitet && result.verarbeitet.length > 0) {
|
||||
result.verarbeitet.forEach(d => {
|
||||
const klasse = d.status === 'sortiert' || d.status === 'direkt_verschoben' ? 'success' :
|
||||
(d.status === 'fehler' ? 'error' : 'info');
|
||||
const icon = d.status === 'sortiert' || d.status === 'direkt_verschoben' ? '✓' :
|
||||
(d.status === 'fehler' ? '✗' : 'ℹ');
|
||||
html += `<div class="log-entry ${klasse}">
|
||||
<span>${icon} ${escapeHtml(d.original)} → ${escapeHtml(d.neuer_name || d.status)}</span>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
logContainer.innerHTML = html;
|
||||
debugLog('Verarbeitung abgeschlossen', 'success');
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
} finally {
|
||||
versteckeLoading();
|
||||
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
|
||||
debugLog('Fehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1012,9 +1029,43 @@ async function ordnerVerarbeiten(id) {
|
|||
|
||||
let editierteRegelId = null;
|
||||
|
||||
// Natürliche Sortierung (Zahlen korrekt: 1, 2, 10, 100 statt 1, 10, 100, 2)
|
||||
function natuerlicheSortierung(a, b) {
|
||||
return a.localeCompare(b, 'de', { numeric: true, sensitivity: 'base' });
|
||||
}
|
||||
|
||||
async function ladeRegeln() {
|
||||
try {
|
||||
const regeln = await api('/regeln');
|
||||
|
||||
// Sortierung anwenden (und in localStorage speichern)
|
||||
const sortSelect = document.getElementById('regeln-sortierung');
|
||||
if (sortSelect) {
|
||||
// Gespeicherte Sortierung wiederherstellen
|
||||
const gespeichert = localStorage.getItem('regeln-sortierung');
|
||||
if (gespeichert && sortSelect.value !== gespeichert) {
|
||||
sortSelect.value = gespeichert;
|
||||
}
|
||||
// Aktuelle Sortierung speichern
|
||||
localStorage.setItem('regeln-sortierung', sortSelect.value);
|
||||
}
|
||||
|
||||
const sortierung = sortSelect?.value || 'name_asc';
|
||||
regeln.sort((a, b) => {
|
||||
switch (sortierung) {
|
||||
case 'name_asc':
|
||||
return natuerlicheSortierung(a.name || '', b.name || '');
|
||||
case 'name_desc':
|
||||
return natuerlicheSortierung(b.name || '', a.name || '');
|
||||
case 'prio_asc':
|
||||
return (a.prioritaet || 0) - (b.prioritaet || 0);
|
||||
case 'prio_desc':
|
||||
return (b.prioritaet || 0) - (a.prioritaet || 0);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
renderRegeln(regeln);
|
||||
} catch (error) {
|
||||
console.error('Fehler:', error);
|
||||
|
|
@ -1059,6 +1110,114 @@ async function kopiereRegel(id) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============ Regeln Import/Export ============
|
||||
|
||||
async function exportiereRegeln() {
|
||||
try {
|
||||
const result = await api('/regeln/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;
|
||||
a.download = `sortierregeln_${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showAlert(`${result.anzahl} Regeln exportiert`, 'success');
|
||||
} catch (error) {
|
||||
showAlert('Fehler beim Export: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function importiereRegeln() {
|
||||
// Datei-Input erstellen
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const regeln = JSON.parse(text);
|
||||
|
||||
// Prüfen ob es ein Array oder ein Objekt mit "regeln" ist
|
||||
const regelListe = Array.isArray(regeln) ? regeln : regeln.regeln;
|
||||
|
||||
if (!regelListe || !Array.isArray(regelListe)) {
|
||||
showAlert('Ungültiges Format: Array von Regeln erwartet', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Import-Modus fragen
|
||||
const modus = await new Promise(resolve => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 400px;">
|
||||
<div class="modal-header">
|
||||
<h3>Import: ${regelListe.length} Regeln</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Wie sollen die Regeln importiert werden?</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: 1rem;">
|
||||
<button class="btn btn-primary" onclick="this.closest('.modal').resolve('hinzufuegen')">
|
||||
Nur neue hinzufügen
|
||||
</button>
|
||||
<button class="btn" onclick="this.closest('.modal').resolve('aktualisieren')">
|
||||
Bestehende aktualisieren
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="this.closest('.modal').resolve('ersetzen')">
|
||||
Alle ersetzen
|
||||
</button>
|
||||
<button class="btn" onclick="this.closest('.modal').resolve(null)">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
modal.resolve = (value) => {
|
||||
modal.remove();
|
||||
resolve(value);
|
||||
};
|
||||
document.body.appendChild(modal);
|
||||
});
|
||||
|
||||
if (!modus) return;
|
||||
|
||||
if (modus === 'ersetzen') {
|
||||
if (!await showConfirm('Alle bestehenden Regeln werden gelöscht. Fortfahren?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await api('/regeln/import', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ regeln: regelListe, modus })
|
||||
});
|
||||
|
||||
showAlert(
|
||||
`Import: ${result.importiert} neu, ${result.aktualisiert} aktualisiert, ${result.uebersprungen} übersprungen`,
|
||||
'success'
|
||||
);
|
||||
ladeRegeln();
|
||||
|
||||
} catch (error) {
|
||||
showAlert('Fehler beim Import: ' + error.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
// ============ Regel-Modal (NEU) ============
|
||||
|
||||
let alleOrdner = []; // Cache für Ordner-Liste
|
||||
|
|
@ -1090,6 +1249,7 @@ async function zeigeRegelModal(regel = null) {
|
|||
const muster = regel?.muster || {};
|
||||
document.getElementById('regel-keywords').value = muster.keywords || '';
|
||||
document.getElementById('regel-keywords-nicht').value = muster.keywords_nicht || '';
|
||||
document.getElementById('regel-auch-dateiname').checked = muster.auch_dateiname || false;
|
||||
document.getElementById('regel-text-regex').value = muster.text_regex || '';
|
||||
|
||||
// Extraktion-Tabelle befüllen
|
||||
|
|
@ -1436,10 +1596,12 @@ async function speichereRegel() {
|
|||
const muster = {};
|
||||
const keywords = document.getElementById('regel-keywords').value.trim();
|
||||
const keywordsNicht = document.getElementById('regel-keywords-nicht').value.trim();
|
||||
const auchDateiname = document.getElementById('regel-auch-dateiname').checked;
|
||||
const textRegex = document.getElementById('regel-text-regex').value.trim();
|
||||
|
||||
if (keywords) muster.keywords = keywords;
|
||||
if (keywordsNicht) muster.keywords_nicht = keywordsNicht;
|
||||
if (auchDateiname) muster.auch_dateiname = true;
|
||||
if (textRegex) muster.text_regex = textRegex;
|
||||
|
||||
// Extraktion aus Tabelle sammeln
|
||||
|
|
@ -2533,14 +2695,641 @@ function truncatePath(path, maxLength = 35) {
|
|||
return `<span title="${escaped}" style="cursor:help;">${escapeHtml(start)}...${escapeHtml(end)}</span>`;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal')) {
|
||||
e.target.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
// Modal nur über X-Button oder Abbrechen schließen, nicht durch Klick auf Hintergrund
|
||||
// (entfernt: Klick auf Modal-Hintergrund schließt nicht mehr)
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.querySelectorAll('.modal:not(.hidden)').forEach(m => m.classList.add('hidden'));
|
||||
}
|
||||
});
|
||||
|
||||
// ============ Tab Navigation ============
|
||||
|
||||
function wechsleTab(tabName) {
|
||||
// Alle Tabs deaktivieren
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Gewählten Tab aktivieren
|
||||
document.querySelector(`.tab-btn[data-tab="${tabName}"]`).classList.add('active');
|
||||
document.getElementById(`tab-${tabName}`).classList.add('active');
|
||||
|
||||
// Tab in localStorage speichern
|
||||
localStorage.setItem('aktiver-tab', tabName);
|
||||
|
||||
// Tab-spezifische Daten laden
|
||||
switch(tabName) {
|
||||
case 'mailabruf':
|
||||
ladePostfaecher();
|
||||
ladeZeitplaeneNachTyp('mail_abruf', 'zeitplaene-mailabruf');
|
||||
break;
|
||||
case 'grobsortierung':
|
||||
ladeOrdner();
|
||||
ladeZeitplaeneNachTyp('grobsortierung', 'zeitplaene-grobsortierung');
|
||||
break;
|
||||
case 'feinsortierung':
|
||||
ladeRegeln();
|
||||
ladeZeitplaeneNachTyp('sortierregeln', 'zeitplaene-feinsortierung');
|
||||
break;
|
||||
case 'dbbackup':
|
||||
ladeDbServer();
|
||||
ladeDatenbanken();
|
||||
ladeBackups();
|
||||
ladeZeitplaeneNachTyp('db_backup', 'zeitplaene-dbbackup');
|
||||
break;
|
||||
}
|
||||
|
||||
ladeStatus();
|
||||
debugLog(`Tab gewechselt: ${tabName}`);
|
||||
}
|
||||
|
||||
// ============ Debug Log Header ============
|
||||
|
||||
let lastDebugMessage = '';
|
||||
|
||||
function debugLog(message, type = 'info') {
|
||||
lastDebugMessage = message;
|
||||
const textEl = document.getElementById('debug-log-text');
|
||||
if (textEl) {
|
||||
textEl.textContent = message;
|
||||
textEl.className = 'debug-log-text ' + type;
|
||||
}
|
||||
console.log(`[DEBUG] ${message}`);
|
||||
}
|
||||
|
||||
// ============ Zeitpläne nach Typ laden ============
|
||||
|
||||
async function ladeZeitplaeneNachTyp(typ, containerId) {
|
||||
try {
|
||||
const zeitplaene = await api('/zeitplaene');
|
||||
const gefiltert = zeitplaene.filter(z => z.typ === typ);
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (gefiltert.length === 0) {
|
||||
container.innerHTML = '<p class="empty-state">Keine Zeitpläne</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = gefiltert.map(z => `
|
||||
<div class="config-item">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Zeitpläne:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Grobsortierung / Alle Ordner verarbeiten ============
|
||||
|
||||
async function alleOrdnerVerarbeiten() {
|
||||
const logContainer = document.getElementById('grobsortierung-log');
|
||||
logContainer.innerHTML = '<div class="log-entry info">Starte Grobsortierung...</div>';
|
||||
|
||||
try {
|
||||
debugLog('Starte Grobsortierung aller Ordner...', 'info');
|
||||
|
||||
const ordner = await api('/ordner');
|
||||
const aktiveOrdner = ordner.filter(o => o.aktiv);
|
||||
|
||||
if (aktiveOrdner.length === 0) {
|
||||
logContainer.innerHTML = '<div class="log-entry warning">Keine aktiven Ordner konfiguriert</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
logContainer.innerHTML = '';
|
||||
let gesamtSortiert = 0;
|
||||
let gesamtFehler = 0;
|
||||
|
||||
for (const o of aktiveOrdner) {
|
||||
debugLog(`Verarbeite: ${o.name}`, 'info');
|
||||
try {
|
||||
const result = await api(`/ordner/${o.id}/verarbeiten`, { method: 'POST' });
|
||||
gesamtSortiert += result.sortiert || 0;
|
||||
|
||||
// Detaillierte Ausgabe pro Ordner
|
||||
let ordnerHtml = `<div class="log-entry success">
|
||||
<strong>✓ ${escapeHtml(o.name)}</strong>: ${result.sortiert || 0} sortiert, ${result.zugferd || 0} ZUGFeRD
|
||||
</div>`;
|
||||
|
||||
// Dateien im Ordner anzeigen
|
||||
if (result.verarbeitet && result.verarbeitet.length > 0) {
|
||||
result.verarbeitet.forEach(d => {
|
||||
const klasse = d.status === 'sortiert' || d.status === 'direkt_verschoben' ? 'success' :
|
||||
(d.status === 'fehler' ? 'error' : 'info');
|
||||
ordnerHtml += `<div class="log-entry ${klasse}" style="padding-left: 2rem;">
|
||||
<span>→ ${escapeHtml(d.original)} → ${escapeHtml(d.neuer_name || d.status)}</span>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
logContainer.innerHTML += ordnerHtml;
|
||||
} catch (error) {
|
||||
gesamtFehler++;
|
||||
logContainer.innerHTML += `<div class="log-entry error">
|
||||
<span>✗ ${escapeHtml(o.name)}: ${error.message}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Zusammenfassung am Ende
|
||||
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
|
||||
<strong>Zusammenfassung:</strong> ${gesamtSortiert} Dateien sortiert, ${gesamtFehler} Fehler
|
||||
</div>`;
|
||||
|
||||
debugLog('Grobsortierung abgeschlossen', 'success');
|
||||
} catch (error) {
|
||||
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
|
||||
debugLog('Fehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Feinsortierung starten ============
|
||||
|
||||
async function feinsortierungStarten() {
|
||||
const logContainer = document.getElementById('sortierung-log');
|
||||
logContainer.innerHTML = '<div class="log-entry info">Starte Feinsortierung...</div>';
|
||||
|
||||
try {
|
||||
debugLog('Starte Feinsortierung mit Live-Updates...', 'info');
|
||||
|
||||
// Streaming-Endpoint verwenden für Live-Updates
|
||||
const response = await fetch('/api/sortierung/starten/stream');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let zusammenfassungDiv = null;
|
||||
let gesamt = 0, sortiert = 0, zugferd = 0, fehler = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // Unvollständige Zeile behalten
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
|
||||
switch (data.type) {
|
||||
case 'start':
|
||||
logContainer.innerHTML = `<div class="log-entry info">
|
||||
<strong>Starte Feinsortierung...</strong> ${data.ordner_count} Ordner, ${data.gesamt} Dateien
|
||||
</div>`;
|
||||
// Zusammenfassungs-Div am Anfang erstellen
|
||||
zusammenfassungDiv = document.createElement('div');
|
||||
zusammenfassungDiv.className = 'log-entry success';
|
||||
zusammenfassungDiv.style.cssText = 'position: sticky; top: 0; background: var(--bg-secondary); border-bottom: 1px solid var(--border); z-index: 1;';
|
||||
zusammenfassungDiv.innerHTML = `<strong>Gesamt: 0 | Sortiert: 0 | ZUGFeRD: 0 | Fehler: 0</strong>`;
|
||||
logContainer.appendChild(zusammenfassungDiv);
|
||||
break;
|
||||
|
||||
case 'ordner':
|
||||
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 0.5rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
|
||||
<strong>📁 ${escapeHtml(data.ordner)}</strong> (${data.dateien} Dateien)
|
||||
</div>`;
|
||||
break;
|
||||
|
||||
case 'datei_start':
|
||||
// Optional: Aktuelle Datei anzeigen
|
||||
break;
|
||||
|
||||
case 'datei_fertig':
|
||||
sortiert++;
|
||||
if (data.zugferd) zugferd++;
|
||||
gesamt++;
|
||||
|
||||
const hatRegel = data.regel;
|
||||
const icon = hatRegel ? '✓' : 'ℹ';
|
||||
|
||||
let text = escapeHtml(data.original || '');
|
||||
if (data.neuer_name) {
|
||||
text += ` → ${escapeHtml(data.neuer_name)}`;
|
||||
}
|
||||
if (data.regel) {
|
||||
text += ` <small style="opacity:0.7">[${escapeHtml(data.regel)}]</small>`;
|
||||
}
|
||||
if (data.zugferd) {
|
||||
text += ` <small style="color:var(--success)">(ZUGFeRD)</small>`;
|
||||
}
|
||||
|
||||
logContainer.innerHTML += `<div class="log-entry success">
|
||||
<span>${icon} ${text}</span>
|
||||
</div>`;
|
||||
|
||||
// Zusammenfassung aktualisieren
|
||||
if (zusammenfassungDiv) {
|
||||
zusammenfassungDiv.innerHTML = `<strong>Gesamt: ${gesamt} | Sortiert: ${sortiert} | ZUGFeRD: ${zugferd} | Fehler: ${fehler}</strong>`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'datei_fehler':
|
||||
fehler++;
|
||||
gesamt++;
|
||||
|
||||
logContainer.innerHTML += `<div class="log-entry error">
|
||||
<span>✗ ${escapeHtml(data.original || '')} <small style="color:var(--danger)">(${escapeHtml(data.fehler || 'Unbekannter Fehler')})</small></span>
|
||||
</div>`;
|
||||
|
||||
// Zusammenfassung aktualisieren
|
||||
if (zusammenfassungDiv) {
|
||||
zusammenfassungDiv.innerHTML = `<strong>Gesamt: ${gesamt} | Sortiert: ${sortiert} | ZUGFeRD: ${zugferd} | Fehler: ${fehler}</strong>`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fertig':
|
||||
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
|
||||
<strong>✓ Feinsortierung abgeschlossen</strong>
|
||||
</div>`;
|
||||
break;
|
||||
}
|
||||
|
||||
// Auto-Scroll zum Ende
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
|
||||
} catch (parseError) {
|
||||
console.error('SSE Parse-Fehler:', parseError, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gesamt === 0) {
|
||||
logContainer.innerHTML = `<div class="log-entry info">Keine Dateien zur Verarbeitung gefunden</div>`;
|
||||
}
|
||||
|
||||
debugLog('Feinsortierung abgeschlossen', 'success');
|
||||
} catch (error) {
|
||||
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
|
||||
debugLog('Fehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Datenbank-Backup Funktionen ============
|
||||
|
||||
let editierterDbServerId = null;
|
||||
let editierteDbId = null;
|
||||
|
||||
async function ladeDbServer() {
|
||||
try {
|
||||
const server = await api('/dbserver');
|
||||
const container = document.getElementById('dbserver-liste');
|
||||
|
||||
if (server.length === 0) {
|
||||
container.innerHTML = '<p class="empty-state">Keine Server konfiguriert</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = server.map(s => `
|
||||
<div class="config-item">
|
||||
<div class="config-item-info">
|
||||
<h4>${escapeHtml(s.name)} ${s.aktiv ? '✓' : ''}</h4>
|
||||
<small>${s.typ} @ ${s.host}:${s.port}</small>
|
||||
</div>
|
||||
<div class="config-item-actions">
|
||||
<button class="btn btn-sm" onclick="dbServerBearbeiten(${s.id})">✏</button>
|
||||
<button class="btn btn-sm" onclick="dbServerAktivieren(${s.id})">${s.aktiv ? '⏸' : '▶'}</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="dbServerLoeschen(${s.id})">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
debugLog('Fehler beim Laden der DB-Server: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function ladeDatenbanken() {
|
||||
try {
|
||||
const dbs = await api('/datenbanken');
|
||||
const container = document.getElementById('datenbanken-liste');
|
||||
|
||||
if (dbs.length === 0) {
|
||||
container.innerHTML = '<p class="empty-state">Keine Datenbanken konfiguriert</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = dbs.map(db => `
|
||||
<div class="config-item">
|
||||
<div class="config-item-info">
|
||||
<h4>${escapeHtml(db.name)} ${db.aktiv ? '✓' : ''}</h4>
|
||||
<small>${escapeHtml(db.database)} (${db.server_name || 'Server'})</small>
|
||||
</div>
|
||||
<div class="config-item-actions">
|
||||
<button class="btn btn-sm btn-success" onclick="dbBackupErstellen(${db.id})" title="Backup jetzt erstellen">💾</button>
|
||||
<button class="btn btn-sm" onclick="dbBearbeiten(${db.id})">✏</button>
|
||||
<button class="btn btn-sm" onclick="dbAktivieren(${db.id})">${db.aktiv ? '⏸' : '▶'}</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="dbLoeschen(${db.id})">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
debugLog('Fehler beim Laden der Datenbanken: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function ladeBackups() {
|
||||
try {
|
||||
const backups = await api('/backups');
|
||||
const container = document.getElementById('backups-liste');
|
||||
|
||||
if (!backups || backups.length === 0) {
|
||||
container.innerHTML = '<p class="empty-state">Keine Backups vorhanden</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = backups.slice(0, 20).map(b => `
|
||||
<div class="log-entry info">
|
||||
<span>${escapeHtml(b.dateiname)}</span>
|
||||
<small>${b.groesse_mb} MB - ${b.erstellt}</small>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
// Backups-Endpoint existiert evtl. noch nicht
|
||||
console.log('Backups-Endpoint nicht verfügbar');
|
||||
}
|
||||
}
|
||||
|
||||
function zeigeDbServerModal(server = null) {
|
||||
editierterDbServerId = server?.id || null;
|
||||
document.getElementById('dbserver-modal-title').textContent = server ? 'DB-Server bearbeiten' : 'DB-Server hinzufügen';
|
||||
|
||||
document.getElementById('dbs-name').value = server?.name || '';
|
||||
document.getElementById('dbs-typ').value = server?.typ || 'mariadb';
|
||||
document.getElementById('dbs-host').value = server?.host || '';
|
||||
document.getElementById('dbs-port').value = server?.port || 3306;
|
||||
document.getElementById('dbs-user').value = server?.user || '';
|
||||
document.getElementById('dbs-password').value = '';
|
||||
|
||||
document.getElementById('dbserver-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function speichereDbServer() {
|
||||
const data = {
|
||||
name: document.getElementById('dbs-name').value.trim(),
|
||||
typ: document.getElementById('dbs-typ').value,
|
||||
host: document.getElementById('dbs-host').value.trim(),
|
||||
port: parseInt(document.getElementById('dbs-port').value) || 3306,
|
||||
user: document.getElementById('dbs-user').value.trim(),
|
||||
password: document.getElementById('dbs-password').value
|
||||
};
|
||||
|
||||
if (!data.name || !data.host || !data.user) {
|
||||
showAlert('Bitte alle Pflichtfelder ausfüllen', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editierterDbServerId) {
|
||||
await api(`/dbserver/${editierterDbServerId}`, { method: 'PUT', body: JSON.stringify(data) });
|
||||
} else {
|
||||
await api('/dbserver', { method: 'POST', body: JSON.stringify(data) });
|
||||
}
|
||||
schliesseModal('dbserver-modal');
|
||||
ladeDbServer();
|
||||
debugLog('DB-Server gespeichert', 'success');
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testeDbServer() {
|
||||
const data = {
|
||||
typ: document.getElementById('dbs-typ').value,
|
||||
host: document.getElementById('dbs-host').value.trim(),
|
||||
port: parseInt(document.getElementById('dbs-port').value) || 3306,
|
||||
user: document.getElementById('dbs-user').value.trim(),
|
||||
password: document.getElementById('dbs-password').value
|
||||
};
|
||||
|
||||
try {
|
||||
zeigeLoading('Teste Verbindung...');
|
||||
const result = await api('/dbserver/test', { method: 'POST', body: JSON.stringify(data) });
|
||||
showAlert(result.message || 'Verbindung erfolgreich!', 'success');
|
||||
} catch (error) {
|
||||
showAlert('Verbindung fehlgeschlagen: ' + error.message, 'error');
|
||||
} finally {
|
||||
versteckeLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function dbServerBearbeiten(id) {
|
||||
try {
|
||||
const server = await api(`/dbserver/${id}`);
|
||||
zeigeDbServerModal(server);
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function dbServerAktivieren(id) {
|
||||
try {
|
||||
await api(`/dbserver/${id}/aktivieren`, { method: 'POST' });
|
||||
ladeDbServer();
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function dbServerLoeschen(id) {
|
||||
if (!await showConfirm('DB-Server wirklich löschen?')) return;
|
||||
try {
|
||||
await api(`/dbserver/${id}`, { method: 'DELETE' });
|
||||
ladeDbServer();
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function zeigeDbModal(db = null) {
|
||||
editierteDbId = db?.id || null;
|
||||
document.getElementById('db-modal-title').textContent = db ? 'Datenbank bearbeiten' : 'Datenbank hinzufügen';
|
||||
|
||||
// Server-Dropdown befüllen
|
||||
const serverSelect = document.getElementById('db-server-id');
|
||||
try {
|
||||
const server = await api('/dbserver');
|
||||
serverSelect.innerHTML = '<option value="">Server wählen...</option>' +
|
||||
server.map(s => `<option value="${s.id}">${escapeHtml(s.name)} (${s.typ})</option>`).join('');
|
||||
} catch (error) {
|
||||
serverSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
|
||||
}
|
||||
|
||||
document.getElementById('db-name').value = db?.name || '';
|
||||
document.getElementById('db-server-id').value = db?.server_id || '';
|
||||
document.getElementById('db-database').value = db?.database || '';
|
||||
document.getElementById('db-backup-pfad').value = db?.backup_pfad || '';
|
||||
document.getElementById('db-aufbewahrung').value = db?.aufbewahrung || 7;
|
||||
document.getElementById('db-format').value = db?.format || 'sql';
|
||||
|
||||
document.getElementById('db-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function speichereDb() {
|
||||
const data = {
|
||||
name: document.getElementById('db-name').value.trim(),
|
||||
server_id: parseInt(document.getElementById('db-server-id').value),
|
||||
database: document.getElementById('db-database').value.trim(),
|
||||
backup_pfad: document.getElementById('db-backup-pfad').value.trim(),
|
||||
aufbewahrung: parseInt(document.getElementById('db-aufbewahrung').value) || 7,
|
||||
format: document.getElementById('db-format').value
|
||||
};
|
||||
|
||||
if (!data.name || !data.server_id || !data.database || !data.backup_pfad) {
|
||||
showAlert('Bitte alle Pflichtfelder ausfüllen', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editierteDbId) {
|
||||
await api(`/datenbanken/${editierteDbId}`, { method: 'PUT', body: JSON.stringify(data) });
|
||||
} else {
|
||||
await api('/datenbanken', { method: 'POST', body: JSON.stringify(data) });
|
||||
}
|
||||
schliesseModal('db-modal');
|
||||
ladeDatenbanken();
|
||||
debugLog('Datenbank gespeichert', 'success');
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function dbBearbeiten(id) {
|
||||
try {
|
||||
const db = await api(`/datenbanken/${id}`);
|
||||
zeigeDbModal(db);
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function dbAktivieren(id) {
|
||||
try {
|
||||
await api(`/datenbanken/${id}/aktivieren`, { method: 'POST' });
|
||||
ladeDatenbanken();
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function dbLoeschen(id) {
|
||||
if (!await showConfirm('Datenbank-Konfiguration wirklich löschen?')) return;
|
||||
try {
|
||||
await api(`/datenbanken/${id}`, { method: 'DELETE' });
|
||||
ladeDatenbanken();
|
||||
} catch (error) {
|
||||
showAlert(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function dbBackupErstellen(id) {
|
||||
const logContainer = document.getElementById('dbbackup-log');
|
||||
logContainer.innerHTML = '<div class="log-entry info">Erstelle Backup...</div>';
|
||||
|
||||
try {
|
||||
debugLog('Erstelle Backup...', 'info');
|
||||
const result = await api(`/datenbanken/${id}/backup`, { method: 'POST' });
|
||||
|
||||
logContainer.innerHTML = `<div class="log-entry success">
|
||||
<span>✓ Backup erstellt: ${escapeHtml(result.datei || 'Erfolgreich')}</span>
|
||||
<small>${result.groesse_mb ? result.groesse_mb + ' MB' : ''}</small>
|
||||
</div>`;
|
||||
|
||||
debugLog('Backup erstellt: ' + (result.datei || 'Erfolgreich'), 'success');
|
||||
ladeBackups();
|
||||
} catch (error) {
|
||||
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
|
||||
debugLog('Backup-Fehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function alleDbBackupsErstellen() {
|
||||
const logContainer = document.getElementById('dbbackup-log');
|
||||
logContainer.innerHTML = '<div class="log-entry info">Starte Backups...</div>';
|
||||
|
||||
try {
|
||||
debugLog('Starte Backup aller Datenbanken...', 'info');
|
||||
|
||||
const dbs = await api('/datenbanken');
|
||||
const aktiveDbs = dbs.filter(db => db.aktiv);
|
||||
|
||||
if (aktiveDbs.length === 0) {
|
||||
logContainer.innerHTML = '<div class="log-entry warning">Keine aktiven Datenbanken konfiguriert</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
logContainer.innerHTML = '';
|
||||
let erfolg = 0;
|
||||
let fehler = 0;
|
||||
|
||||
for (const db of aktiveDbs) {
|
||||
debugLog(`Backup: ${db.name}`, 'info');
|
||||
try {
|
||||
const result = await api(`/datenbanken/${db.id}/backup`, { method: 'POST' });
|
||||
erfolg++;
|
||||
logContainer.innerHTML += `<div class="log-entry success">
|
||||
<span>✓ ${escapeHtml(db.name)}: ${escapeHtml(result.datei || 'OK')}</span>
|
||||
<small>${result.groesse_mb ? result.groesse_mb + ' MB' : ''}</small>
|
||||
</div>`;
|
||||
} catch (error) {
|
||||
fehler++;
|
||||
logContainer.innerHTML += `<div class="log-entry error">
|
||||
<span>✗ ${escapeHtml(db.name)}: ${error.message}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Zusammenfassung
|
||||
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
|
||||
<strong>Zusammenfassung:</strong> ${erfolg} erfolgreich, ${fehler} Fehler
|
||||
</div>`;
|
||||
|
||||
debugLog('Alle Backups abgeschlossen', 'success');
|
||||
ladeBackups();
|
||||
} catch (error) {
|
||||
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
|
||||
debugLog('Fehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Zeitplan Modal erweitert ============
|
||||
|
||||
function zeitplanTypChanged() {
|
||||
const typ = document.getElementById('zp-typ').value;
|
||||
document.getElementById('zp-postfach-gruppe').classList.toggle('hidden', typ !== 'mail_abruf');
|
||||
document.getElementById('zp-ordner-gruppe').classList.toggle('hidden', typ !== 'grobsortierung');
|
||||
document.getElementById('zp-regel-gruppe').classList.toggle('hidden', typ !== 'sortierregeln');
|
||||
document.getElementById('zp-db-gruppe').classList.toggle('hidden', typ !== 'db_backup');
|
||||
|
||||
// Datenbanken-Dropdown befüllen wenn DB-Backup gewählt
|
||||
if (typ === 'db_backup') {
|
||||
ladeDbFuerZeitplan();
|
||||
}
|
||||
}
|
||||
|
||||
async function ladeDbFuerZeitplan() {
|
||||
try {
|
||||
const dbs = await api('/datenbanken');
|
||||
const select = document.getElementById('zp-db');
|
||||
select.innerHTML = '<option value="">Alle aktiven Datenbanken</option>' +
|
||||
dbs.map(db => `<option value="${db.id}">${escapeHtml(db.name)}</option>`).join('');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Datenbanken für Zeitplan:', error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@
|
|||
<div class="header-left">
|
||||
<h1>Dateiverwaltung</h1>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<!-- Debug-Log Anzeige -->
|
||||
<div id="debug-log-header" class="debug-log-header">
|
||||
<span class="debug-log-label">Log:</span>
|
||||
<span id="debug-log-text" class="debug-log-text">Bereit</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span id="status-indicator"></span>
|
||||
<button class="btn-icon" onclick="zeigeLogModal()" title="Debug-Log">📋</button>
|
||||
|
|
@ -20,20 +27,30 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-container">
|
||||
<!-- Bereich 1: Mail-Abruf -->
|
||||
<section class="bereich">
|
||||
<div class="bereich-header">
|
||||
<h2>📧 Mail-Abruf</h2>
|
||||
<p class="bereich-desc">Attachments aus Postfächern in Ordner speichern</p>
|
||||
</div>
|
||||
<!-- Tab-Navigation -->
|
||||
<nav class="tab-navigation">
|
||||
<button class="tab-btn active" data-tab="mailabruf" onclick="wechsleTab('mailabruf')">
|
||||
📧 Mailabruf
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="grobsortierung" onclick="wechsleTab('grobsortierung')">
|
||||
📁 Grobsortierung
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="feinsortierung" onclick="wechsleTab('feinsortierung')">
|
||||
📑 Feinsortierung
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="dbbackup" onclick="wechsleTab('dbbackup')">
|
||||
🗄️ Datenbank-Backup
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="bereich-content">
|
||||
<!-- Postfächer Liste -->
|
||||
<!-- Tab: Mailabruf -->
|
||||
<div id="tab-mailabruf" class="tab-content active">
|
||||
<div class="tab-layout">
|
||||
<!-- Links: Postfächer -->
|
||||
<section class="tab-left">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Postfächer</h3>
|
||||
<h3>📧 Postfächer</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigePostfachModal()">+ Hinzufügen</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
@ -42,15 +59,23 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Abruf starten -->
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-success btn-large" onclick="allePostfaecherAbrufen()">
|
||||
▶ Alle Postfächer abrufen
|
||||
</button>
|
||||
<!-- Mitte: Abruf-Bereich -->
|
||||
<section class="tab-center">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Aktion</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-success btn-large" onclick="allePostfaecherAbrufen()">
|
||||
▶ Alle Postfächer abrufen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Letzter Abruf Log -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Letzter Abruf</h3>
|
||||
|
|
@ -61,35 +86,130 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Bereich 2: Datei-Sortierung -->
|
||||
<section class="bereich">
|
||||
<div class="bereich-header">
|
||||
<h2>📁 Datei-Sortierung</h2>
|
||||
<p class="bereich-desc">Dateien nach Regeln umbenennen und verschieben</p>
|
||||
</div>
|
||||
|
||||
<div class="bereich-content">
|
||||
<!-- Grobsortierung -->
|
||||
<!-- Rechts: Status & Scheduler -->
|
||||
<section class="tab-right">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Grobsortierung</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeOrdnerModal()">+ Hinzufügen</button>
|
||||
<h3>⏰ Status</h3>
|
||||
<button class="btn btn-sm" onclick="ladeStatus()">🔄</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="ordner-liste">
|
||||
<p class="empty-state">Keine Ordner konfiguriert</p>
|
||||
<div id="status-mailabruf">
|
||||
<p class="empty-state">Status wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regeln -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Sortier-Regeln</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeRegelModal()">+ Hinzufügen</button>
|
||||
<h3>Zeitpläne</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeZeitplanModal('mail_abruf')">+ Neu</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="zeitplaene-mailabruf">
|
||||
<p class="empty-state">Keine Zeitpläne</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Grobsortierung -->
|
||||
<div id="tab-grobsortierung" class="tab-content">
|
||||
<div class="tab-layout">
|
||||
<!-- Links: Grobsortierung-Regeln -->
|
||||
<section class="tab-left">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📁 Grobsortierung</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeOrdnerModal()">+ Hinzufügen</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="ordner-liste">
|
||||
<p class="empty-state">Keine Grobsortierung konfiguriert</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mitte: Verarbeitung -->
|
||||
<section class="tab-center">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Aktion</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-success btn-large" onclick="alleOrdnerVerarbeiten()">
|
||||
▶ Alle verarbeiten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Verarbeitungs-Log</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="grobsortierung-log" class="log-output">
|
||||
<p class="empty-state">Noch keine Verarbeitung durchgeführt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rechts: Status & Scheduler -->
|
||||
<section class="tab-right">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>⏰ Status</h3>
|
||||
<button class="btn btn-sm" onclick="ladeStatus()">🔄</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="status-grobsortierung">
|
||||
<p class="empty-state">Status wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Zeitpläne</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeZeitplanModal('grobsortierung')">+ Neu</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="zeitplaene-grobsortierung">
|
||||
<p class="empty-state">Keine Zeitpläne</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Feinsortierung -->
|
||||
<div id="tab-feinsortierung" class="tab-content">
|
||||
<div class="tab-layout">
|
||||
<!-- Links: Sortier-Regeln -->
|
||||
<section class="tab-left">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📑 Sortier-Regeln</h3>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<select id="regeln-sortierung" onchange="ladeRegeln()" title="Sortierung" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">
|
||||
<option value="name_asc">Name A-Z</option>
|
||||
<option value="name_desc">Name Z-A</option>
|
||||
<option value="prio_asc">Priorität ↑</option>
|
||||
<option value="prio_desc">Priorität ↓</option>
|
||||
</select>
|
||||
<button class="btn btn-sm" onclick="exportiereRegeln()" title="Regeln exportieren">📤</button>
|
||||
<button class="btn btn-sm" onclick="importiereRegeln()" title="Regeln importieren">📥</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeRegelModal()">+ Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="regeln-liste">
|
||||
|
|
@ -97,15 +217,23 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sortierung starten -->
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-success btn-large" onclick="sortierungStarten()">
|
||||
▶ Sortierung starten
|
||||
</button>
|
||||
<!-- Mitte: Verarbeitung -->
|
||||
<section class="tab-center">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Aktion</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-success btn-large" onclick="feinsortierungStarten()">
|
||||
▶ Feinsortierung starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sortierungs-Log -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Verarbeitete Dateien</h3>
|
||||
|
|
@ -116,44 +244,132 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Bereich 3: Zeitpläne / Scheduler -->
|
||||
<section class="bereich">
|
||||
<div class="bereich-header">
|
||||
<h2>⏰ Zeitpläne</h2>
|
||||
<p class="bereich-desc">Automatische Ausführung von Mail-Abruf und Sortierung</p>
|
||||
</div>
|
||||
|
||||
<div class="bereich-content">
|
||||
<!-- Status-Übersicht -->
|
||||
<!-- Rechts: Status & Scheduler -->
|
||||
<section class="tab-right">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Status-Übersicht</h3>
|
||||
<button class="btn btn-sm" onclick="ladeStatus()">🔄 Aktualisieren</button>
|
||||
<h3>⏰ Status</h3>
|
||||
<button class="btn btn-sm" onclick="ladeStatus()">🔄</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="status-uebersicht">
|
||||
<div id="status-feinsortierung">
|
||||
<p class="empty-state">Status wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitpläne Liste -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Zeitpläne</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeZeitplanModal()">+ Hinzufügen</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeZeitplanModal('sortierregeln')">+ Neu</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="zeitplaene-liste">
|
||||
<p class="empty-state">Keine Zeitpläne konfiguriert</p>
|
||||
<div id="zeitplaene-feinsortierung">
|
||||
<p class="empty-state">Keine Zeitpläne</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Datenbank-Backup -->
|
||||
<div id="tab-dbbackup" class="tab-content">
|
||||
<div class="tab-layout">
|
||||
<!-- Links: DB-Server & Datenbanken -->
|
||||
<section class="tab-left">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🗄️ DB-Server</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeDbServerModal()">+ Server</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dbserver-liste">
|
||||
<p class="empty-state">Keine Server konfiguriert</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📊 Datenbanken</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeDbModal()">+ Datenbank</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="datenbanken-liste">
|
||||
<p class="empty-state">Keine Datenbanken konfiguriert</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mitte: Backup-Bereich -->
|
||||
<section class="tab-center">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Aktion</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-success btn-large" onclick="alleDbBackupsErstellen()">
|
||||
▶ Alle Backups erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Backup-Log</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dbbackup-log" class="log-output">
|
||||
<p class="empty-state">Noch keine Backups durchgeführt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Vorhandene Backups</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="backups-liste">
|
||||
<p class="empty-state">Keine Backups vorhanden</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rechts: Status & Scheduler -->
|
||||
<section class="tab-right">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>⏰ Status</h3>
|
||||
<button class="btn btn-sm" onclick="ladeStatus()">🔄</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="status-dbbackup">
|
||||
<p class="empty-state">Status wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Zeitpläne</h3>
|
||||
<button class="btn btn-sm btn-primary" onclick="zeigeZeitplanModal('db_backup')">+ Neu</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="zeitplaene-dbbackup">
|
||||
<p class="empty-state">Keine Zeitpläne</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Postfach hinzufügen -->
|
||||
|
|
@ -266,7 +482,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Ordner hinzufügen/bearbeiten - Breit mit 2 Spalten -->
|
||||
<!-- Modal: Grobsortierung hinzufügen/bearbeiten -->
|
||||
<div id="ordner-modal" class="modal hidden">
|
||||
<div class="modal-content modal-fullwidth">
|
||||
<div class="modal-header">
|
||||
|
|
@ -499,6 +715,12 @@
|
|||
<label title="Komma-getrennte Wörter die NICHT im Dokument vorkommen dürfen">Ausschluss-Keywords</label>
|
||||
<input type="text" id="regel-keywords-nicht" placeholder="gutschrift, storno" title="Wenn eines dieser Wörter vorkommt, greift die Regel NICHT. Nützlich um z.B. Gutschriften von Rechnungen zu unterscheiden.">
|
||||
</div>
|
||||
<div class="form-row" style="margin-top: 0.5rem;">
|
||||
<label class="checkbox-label compact" title="Wenn aktiviert, werden Keywords auch im Dateinamen gesucht (Standard: nur PDF-Text)">
|
||||
<input type="checkbox" id="regel-auch-dateiname">
|
||||
<span>Auch Dateinamen prüfen</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feld-Extraktion -->
|
||||
|
|
@ -612,6 +834,105 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: DB-Server hinzufügen -->
|
||||
<div id="dbserver-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="dbserver-modal-title">DB-Server hinzufügen</h3>
|
||||
<button class="modal-close" onclick="schliesseModal('dbserver-modal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" id="dbs-name" placeholder="z.B. Nextcloud Server">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Typ</label>
|
||||
<select id="dbs-typ">
|
||||
<option value="mariadb">MariaDB</option>
|
||||
<option value="mysql">MySQL</option>
|
||||
<option value="postgresql">PostgreSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Host / IP</label>
|
||||
<input type="text" id="dbs-host" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Port</label>
|
||||
<input type="number" id="dbs-port" value="3306">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Benutzer</label>
|
||||
<input type="text" id="dbs-user" placeholder="root">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" id="dbs-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" onclick="schliesseModal('dbserver-modal')">Abbrechen</button>
|
||||
<button class="btn" onclick="testeDbServer()">🔌 Verbindung testen</button>
|
||||
<button class="btn btn-primary" onclick="speichereDbServer()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Datenbank hinzufügen -->
|
||||
<div id="db-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="db-modal-title">Datenbank hinzufügen</h3>
|
||||
<button class="modal-close" onclick="schliesseModal('db-modal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Name (zur Identifikation)</label>
|
||||
<input type="text" id="db-name" placeholder="z.B. Nextcloud DB">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Server</label>
|
||||
<select id="db-server-id">
|
||||
<option value="">Server wählen...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Datenbank-Name</label>
|
||||
<input type="text" id="db-database" placeholder="nextcloud">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Backup-Zielordner</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="db-backup-pfad" placeholder="/mnt/backups/datenbanken/">
|
||||
<button class="btn" type="button" onclick="oeffneBrowser('db-backup-pfad')">📁</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Backups behalten</label>
|
||||
<input type="number" id="db-aufbewahrung" value="7" min="1" style="width: 80px;">
|
||||
<small>Anzahl der aufzubewahrenden Backups</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Format</label>
|
||||
<select id="db-format">
|
||||
<option value="sql">SQL-Datei (.sql)</option>
|
||||
<option value="sql.gz">Komprimiert (.sql.gz)</option>
|
||||
<option value="zip">ZIP-Archiv (.zip)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" onclick="schliesseModal('db-modal')">Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick="speichereDb()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Verzeichnis-Browser -->
|
||||
<div id="browser-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
|
|
@ -653,9 +974,10 @@
|
|||
<div class="form-group">
|
||||
<label>Was soll ausgeführt werden?</label>
|
||||
<select id="zp-typ" onchange="zeitplanTypChanged()">
|
||||
<option value="mail_abruf">Mail-Abruf</option>
|
||||
<option value="mail_abruf">Mailabruf</option>
|
||||
<option value="grobsortierung">Grobsortierung</option>
|
||||
<option value="sortierregeln">Nur Sortierregeln</option>
|
||||
<option value="sortierregeln">Feinsortierung</option>
|
||||
<option value="db_backup">Datenbank-Backup</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -680,6 +1002,13 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group hidden" id="zp-db-gruppe">
|
||||
<label>Datenbank (leer = alle aktiven)</label>
|
||||
<select id="zp-db">
|
||||
<option value="">Alle aktiven Datenbanken</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Intervall</label>
|
||||
<select id="zp-intervall" onchange="zeitplanIntervallChanged()">
|
||||
|
|
|
|||
Loading…
Reference in a new issue