import os import json import threading import time import sqlite3 import logging import shutil import requests import whisper import re import uuid import base64 import traceback import gc import py_compile import subprocess from datetime import datetime, timedelta from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory, session # --- SETUP --- app = Flask(__name__) app.secret_key = "geheim_schluessel_bitte_aendern" CONFIG_FILE = "config.json" DB_FILE = "voicemails.db" DOWNLOAD_DIR = "recordings" TEMP_DIR = "temp_uploads" BASE_DIR = os.path.dirname(os.path.abspath(__file__)) TEMPLATES_DIR = os.path.join(BASE_DIR, "templates") # Logging Setup logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("app.log", encoding='utf-8'), logging.StreamHandler() ] ) for d in [DOWNLOAD_DIR, TEMP_DIR, TEMPLATES_DIR]: if not os.path.exists(d): os.makedirs(d) def load_config(): defaults = { "placetel_api_token": "", "check_interval_minutes": 10, "delete_recordings_after_days": 7, "standard_export_path": "/opt/comhub/share/Export", "fax_outbox_path": "/opt/comhub/share/FaxOut", "fax_from_number": "", "fax_email": "", "admin_pin": "1234", "admin_password": "6hJUE#MM", "review_pin_enabled": "false", "review_pin": "0000", "module_voicemail": "true", "module_transcription": "true", "module_fax": "true", "module_gdt": "false", "module_docs": "true", "gdt_import_path": "/opt/comhub/share/GDT/Import", "gdt_export_path": "/opt/comhub/share/GDT/Export", "gdt_sender_id": "COMHUB", "gdt_receiver_id": "PVS", "monitored_folder_path": "/opt/comhub/share/Dokumente/Import", "allow_retranscription": "false" } if not os.path.exists(CONFIG_FILE): return defaults with open(CONFIG_FILE, 'r') as f: data = json.load(f) if "check_interval_seconds" in data and "check_interval_minutes" not in data: data["check_interval_minutes"] = max(1, data["check_interval_seconds"] // 60) del data["check_interval_seconds"] if "fax_export_path" in data and "standard_export_path" not in data: data["standard_export_path"] = data["fax_export_path"] del data["fax_export_path"] if "gdt_enabled" in data and "module_gdt" not in data: data["module_gdt"] = data["gdt_enabled"] if "folder_monitoring_enabled" in data and "module_docs" not in data: data["module_docs"] = data["folder_monitoring_enabled"] path_keys = ["standard_export_path", "fax_outbox_path", "gdt_import_path", "gdt_export_path", "monitored_folder_path"] for key in path_keys: if key in data and data[key].startswith("C:/"): data[key] = defaults[key] for key, val in defaults.items(): if key not in data: data[key] = val return data def save_config(new_config): with open(CONFIG_FILE, 'w') as f: json.dump(new_config, f, indent=4) config = load_config() if not os.path.exists(config["fax_outbox_path"]): try: os.makedirs(config["fax_outbox_path"]) except: pass # --- DATENBANK --- def get_db_connection(): conn = sqlite3.connect(DB_FILE, timeout=15.0) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL;") return conn def init_db(): conn = get_db_connection() conn.execute(""" CREATE TABLE IF NOT EXISTS voicemails ( id TEXT PRIMARY KEY, audio_path TEXT, transcription TEXT, status TEXT DEFAULT 'Neu', received_at TEXT, is_archived INTEGER DEFAULT 0, sender TEXT ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS faxes ( id TEXT PRIMARY KEY, sender TEXT, file_path TEXT, status TEXT DEFAULT 'Neu', received_at TEXT ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS faxes_out ( id TEXT PRIMARY KEY, file_path TEXT, recipient TEXT, note TEXT, status TEXT DEFAULT 'Entwurf', created_at TEXT ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS contacts ( id TEXT PRIMARY KEY, first_name TEXT, last_name TEXT, number TEXT, company TEXT, fax_number TEXT, patient_id TEXT ) """) try: conn.execute("ALTER TABLE voicemails ADD COLUMN sender TEXT") except: pass try: conn.execute("ALTER TABLE faxes_out ADD COLUMN note TEXT") except: pass try: conn.execute("ALTER TABLE contacts ADD COLUMN fax_number TEXT") except: pass try: conn.execute("ALTER TABLE faxes_out ADD COLUMN placetel_id TEXT") except: pass try: conn.execute("ALTER TABLE contacts ADD COLUMN patient_id TEXT") except: pass try: conn.execute("ALTER TABLE faxes ADD COLUMN source TEXT DEFAULT 'api'") except: pass try: conn.execute("ALTER TABLE faxes ADD COLUMN title TEXT") except: pass conn.commit() conn.close() init_db() # --- HELPER --- def normalize_number(num): if not num: return "" s = str(num).strip() s = re.sub(r'[^0-9+]', '', s) if s.startswith("+49"): s = "0" + s[3:] elif s.startswith("0049"): s = "0" + s[4:] return re.sub(r'[^0-9]', '', s) def format_german_date(date_str): if not date_str: return "" try: dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M") return dt.strftime("%d-%m-%y %H:%M") except: return date_str def extract_pdf_title(filepath, default_sender): """Hybrider PDF-Scanner (PyPDF2 für digitale Dokumente, Tesseract-OCR für Faxe)""" text = "" # 1. Versuch: Normalen Text extrahieren (schnell) try: import PyPDF2 with open(filepath, 'rb') as f: reader = PyPDF2.PdfReader(f) if len(reader.pages) > 0: text = reader.pages[0].extract_text() except ImportError: logging.warning("PyPDF2 nicht installiert.") except Exception as e: logging.error(f"Fehler bei PyPDF2 Textauslesung: {e}") # 2. Versuch: Wenn der Text extrem kurz ist (meist bei Fax-Bildern), nutze OCR if not text or len(text.strip()) < 20: try: from pdf2image import convert_from_path import pytesseract logging.info(f"Führe OCR auf Dokument {filepath} aus...") images = convert_from_path(filepath, first_page=1, last_page=1) if images: # Nutze deutsches Sprachpaket, falls installiert text = pytesseract.image_to_string(images[0], lang='deu') except ImportError: logging.warning("OCR (pdf2image/pytesseract) nicht installiert. Überspringe Bild-Erkennung.") except Exception as e: logging.error(f"Fehler bei OCR-Verarbeitung: {e}") # Fallback, falls absolut kein Text gefunden wurde if not text or not text.strip(): return f"Dokument von {default_sender}" # 3. Text analysieren und cleveren Titel bauen text_upper = text.upper() doc_type = "Dokument" # Typische medizinische/geschäftliche Dokumenten-Arten keywords = { "BEFUND": "Befund", "ARZTBRIEF": "Arztbrief", "RECHNUNG": "Rechnung", "ÜBERWEISUNG": "Überweisung", "LABOR": "Labor", "REZEPT": "Rezept", "VERORDNUNG": "Verordnung", "ENTLASSUNG": "Entlassungsbericht", "AU-BESCHEINIGUNG": "AU-Bescheinigung", "ARBEITSUNFÄHIGKEIT": "AU-Bescheinigung", "HEILMITTEL": "Heilmittel-Verordnung", "ANTRAG": "Antrag" } for kw, readable in keywords.items(): if kw in text_upper: doc_type = readable break # Versuch, den Namen/eine Betreffzeile zu extrahieren (erste sinnvolle Zeile ohne Rauschen) lines = [line.strip() for line in text.split('\n') if line.strip()] # Filtere Zeilen raus, die nur aus Zahlen/Sonderzeichen bestehen valid_lines = [l for l in lines if len(re.sub(r'[^a-zA-ZäöüÄÖÜß]', '', l)) > 4] title_suffix = default_sender if valid_lines: for line in valid_lines[:5]: # Prüfe die ersten 5 validen Zeilen clean_line = re.sub(r'[^a-zA-Z0-9äöüÄÖÜß \-,\.]', '', line).strip() # Ignoriere Zeilen, die exakt wie das Keyword heißen oder zu kurz sind if len(clean_line) > 5 and clean_line.upper() not in keywords: title_suffix = clean_line[:40] break return f"{doc_type} - {title_suffix}" def get_contact_lookup(): conn = get_db_connection() rows = conn.execute("SELECT * FROM contacts").fetchall() conn.close() lookup = {} for row in rows: r_dict = dict(row) clean_num = normalize_number(r_dict.get('number', '')) clean_fax = normalize_number(r_dict.get('fax_number', '')) name = f"{r_dict.get('first_name', '')} {r_dict.get('last_name', '')}".strip() if r_dict.get('company'): name += f" ({r_dict.get('company')})" if r_dict.get('patient_id'): name = f"[{r_dict.get('patient_id')}] " + name if clean_num: lookup[clean_num] = name if clean_fax: lookup[clean_fax] = name return lookup def resolve_faxes_with_names(faxes_rows): contact_map = get_contact_lookup() results = [] for fax in faxes_rows: f = dict(fax) f['received_formatted'] = format_german_date(f.get('received_at', '')) f['title'] = f.get('title') or "Eingehendes Dokument" sender_clean = normalize_number(f.get('sender', '')) f['resolved_name'] = f.get('sender', '') if sender_clean in contact_map: f['resolved_name'] = contact_map[sender_clean] else: for c_num, c_name in contact_map.items(): if len(c_num) > 6 and (sender_clean.endswith(c_num) or c_num.endswith(sender_clean)): f['resolved_name'] = c_name break results.append(f) return results def resolve_outbox_with_names(faxes_out_rows): contact_map = get_contact_lookup() results = [] for out in faxes_out_rows: o = dict(out) o['created_formatted'] = format_german_date(o.get('created_at', '')) recip_clean = normalize_number(o.get('recipient', '')) o['resolved_name'] = o.get('recipient', '') if recip_clean in contact_map: o['resolved_name'] = contact_map[recip_clean] else: for c_num, c_name in contact_map.items(): if len(c_num) > 6 and (recip_clean.endswith(c_num) or c_num.endswith(recip_clean)): o['resolved_name'] = c_name break results.append(o) return results # --- CONTEXT PROCESSOR --- @app.context_processor def inject_globals(): counts = dict(cnt_vm=0, cnt_fax=0, cnt_rev=0, cnt_out=0, cnt_doc=0) try: conn = get_db_connection() counts['cnt_vm'] = conn.execute("SELECT count(*) FROM voicemails WHERE status='Neu' AND is_archived=0").fetchone()[0] counts['cnt_fax'] = conn.execute("SELECT count(*) FROM faxes WHERE status='Neu' AND IFNULL(source, 'api') = 'api'").fetchone()[0] counts['cnt_doc'] = conn.execute("SELECT count(*) FROM faxes WHERE status='Neu' AND source = 'folder'").fetchone()[0] counts['cnt_rev'] = conn.execute("SELECT count(*) FROM faxes WHERE status='Prüfung'").fetchone()[0] counts['cnt_out'] = conn.execute("SELECT count(*) FROM faxes_out WHERE status='Entwurf'").fetchone()[0] conn.close() except: pass is_auth = session.get('authenticated', False) return dict(**counts, is_authenticated=is_auth, config=config) # --- BACKGROUND WORKER --- class BackgroundWorker: def __init__(self): self.is_running = False self.sync_lock = threading.Lock() self.stop_event = threading.Event() self.update_config() def update_config(self): self.interval = int(config.get("check_interval_minutes", 10)) self.retention = int(config.get("delete_recordings_after_days", 7)) self.api_token = config.get("placetel_api_token", "") self.module_voicemail = str(config.get("module_voicemail", "true")).lower() == "true" self.module_transcription = str(config.get("module_transcription", "true")).lower() == "true" self.module_fax = str(config.get("module_fax", "true")).lower() == "true" self.module_gdt = str(config.get("module_gdt", "false")).lower() == "true" self.module_docs = str(config.get("module_docs", "true")).lower() == "true" self.outbox_path = config.get("fax_outbox_path", "/opt/comhub/share/FaxOut") self.gdt_import_path = config.get("gdt_import_path", "/opt/comhub/share/GDT/Import") self.monitored_folder_path = config.get("monitored_folder_path", "/opt/comhub/share/Dokumente/Import") if self.module_fax and not os.path.exists(self.outbox_path): try: os.makedirs(self.outbox_path) except: pass if self.module_gdt and not os.path.exists(self.gdt_import_path): try: os.makedirs(self.gdt_import_path) except: pass if self.module_docs and not os.path.exists(self.monitored_folder_path): try: os.makedirs(self.monitored_folder_path) except: pass def start_scheduler(self): logging.info("Start Scheduler...") thread = threading.Thread(target=self._loop, daemon=True) thread.start() def _loop(self): last_sync = 0 while not self.stop_event.is_set(): if self.module_fax: self._process_outbox() if self.module_gdt: self._process_gdt_import() if self.module_docs: self._process_monitored_folder() if self.module_transcription: self._process_transcriptions() now = time.time() if now - last_sync >= (self.interval * 60): if self.api_token: self._process_unified_sync() self._process_contacts() if self.module_fax: self._process_outbox_status() self._cleanup() last_sync = now for _ in range(5): if self.stop_event.is_set(): break time.sleep(1) def sync_now(self): if self.sync_lock.locked(): return "läuft bereits" threading.Thread(target=self._run_once, daemon=True).start() return "gestartet" def _run_once(self): if self.module_gdt: self._process_gdt_import() if self.module_docs: self._process_monitored_folder() if self.api_token: self._process_unified_sync() self._process_contacts() if self.module_fax: self._process_outbox_status() def _process_transcriptions(self): conn = get_db_connection() pending = conn.execute("SELECT id, audio_path FROM voicemails WHERE transcription = '[Transkription wird vorbereitet...]'").fetchall() conn.close() if not pending: return logging.info(f"{len(pending)} Voicemail(s) warten auf Transkription.") local_whisper_model = None try: for vm in pending: conn = get_db_connection() conn.execute("UPDATE voicemails SET transcription = '[Transkription läuft...]' WHERE id = ?", (vm['id'],)) conn.commit() conn.close() lpath = os.path.join(DOWNLOAD_DIR, vm['audio_path']) txt = "" if not os.path.exists(lpath) or os.path.getsize(lpath) < 1024: logging.warning(f"Audiodatei {vm['audio_path']} ist defekt oder leer.") txt = "[Fehler: Audiodatei defekt oder leer]" else: try: if not local_whisper_model: logging.info("Lade Whisper Modell in den Arbeitsspeicher...") model_dir = os.path.join(BASE_DIR, "models") os.makedirs(model_dir, exist_ok=True) local_whisper_model = whisper.load_model("turbo", download_root=model_dir) res = local_whisper_model.transcribe( lpath, fp16=False, language="de", initial_prompt="Hallo, dies ist eine Sprachnachricht. Bitte rufen Sie mich zurück.", condition_on_previous_text=False, beam_size=5, no_speech_threshold=0.6 ) txt = res["text"].strip() if not txt: txt = "[Keine Sprache erkannt]" except Exception as ex: logging.error(f"Whisper Transkriptionsfehler für Datei {lpath}: {ex}\n{traceback.format_exc()}") txt = "[Fehler bei der Transkription]" conn = get_db_connection() conn.execute("UPDATE voicemails SET transcription = ? WHERE id = ?", (txt, vm['id'])) conn.commit() conn.close() logging.info(f"Transkription für {vm['id']} abgeschlossen.") finally: if local_whisper_model is not None: del local_whisper_model gc.collect() logging.info("Whisper Modell aus dem Arbeitsspeicher entfernt.") def _process_monitored_folder(self): if not os.path.exists(self.monitored_folder_path): return try: for filename in os.listdir(self.monitored_folder_path): if filename.lower().endswith(".pdf"): src = os.path.join(self.monitored_folder_path, filename) try: initial_size = os.path.getsize(src) if initial_size == 0: continue time.sleep(1.0) if os.path.getsize(src) != initial_size: continue os.rename(src, src) with open(src, 'rb') as check_f: if not check_f.read(5).startswith(b'%PDF-'): continue except OSError: continue except Exception: continue doc_id = uuid.uuid4().hex[:12] safe_name = f"doc_{doc_id}.pdf" dst = os.path.join(DOWNLOAD_DIR, safe_name) try: shutil.move(src, dst) # Generiere dynamischen Titel via OCR/PyPDF2 doc_title = extract_pdf_title(dst, "Ordner-Import") conn = get_db_connection() date_s = datetime.now().strftime("%Y-%m-%d %H:%M") conn.execute("INSERT INTO faxes (id, sender, file_path, status, received_at, source, title) VALUES (?, ?, ?, 'Neu', ?, 'folder', ?)", (doc_id, filename, safe_name, date_s, doc_title)) conn.commit() conn.close() logging.info(f"Neues Dokument aus Ordner importiert: {filename}") except PermissionError: pass except Exception as e: logging.error(f"Ordner Überwachung Fehler: {e}") def _process_gdt_import(self): if not os.path.exists(self.gdt_import_path): return try: for filename in os.listdir(self.gdt_import_path): if filename.lower().endswith(('.gdt', '.bdt')): filepath = os.path.join(self.gdt_import_path, filename) try: initial_size = os.path.getsize(filepath) if initial_size == 0: continue time.sleep(0.5) if os.path.getsize(filepath) != initial_size: continue os.rename(filepath, filepath) except Exception: continue patient_data = {} try: with open(filepath, 'r', encoding='cp437') as f: for line in f: if len(line) > 7: field = line[3:7] val = line[7:].strip() if field == "3000": patient_data['id'] = val elif field == "3101": patient_data['last_name'] = val elif field == "3102": patient_data['first_name'] = val except Exception as e: logging.error(f"Fehler beim Lesen der GDT Datei {filename}: {e}") continue if 'id' in patient_data: conn = get_db_connection() exists = conn.execute("SELECT id FROM contacts WHERE patient_id = ?", (patient_data['id'],)).fetchone() fname = patient_data.get('first_name', '') lname = patient_data.get('last_name', '') if exists: conn.execute("UPDATE contacts SET first_name = ?, last_name = ? WHERE patient_id = ?", (fname, lname, patient_data['id'])) else: c_id = f"gdt_{uuid.uuid4().hex[:12]}" conn.execute("INSERT INTO contacts (id, first_name, last_name, patient_id) VALUES (?, ?, ?, ?)", (c_id, fname, lname, patient_data['id'])) conn.commit() conn.close() logging.info(f"GDT Patient importiert/aktualisiert: {patient_data['id']}") try: os.remove(filepath) except: pass except Exception as e: logging.error(f"GDT Import Prozess Fehler: {e}") def _process_outbox(self): if not os.path.exists(self.outbox_path): return try: for filename in os.listdir(self.outbox_path): if filename.lower().endswith(".pdf"): src = os.path.join(self.outbox_path, filename) try: initial_size = os.path.getsize(src) if initial_size == 0: continue time.sleep(1.0) if os.path.getsize(src) != initial_size: continue os.rename(src, src) with open(src, 'rb') as check_f: if not check_f.read(5).startswith(b'%PDF-'): continue except OSError: continue except Exception as e: continue out_id = uuid.uuid4().hex[:12] safe_name = f"out_{out_id}.pdf" dst = os.path.join(DOWNLOAD_DIR, safe_name) try: shutil.move(src, dst) conn = get_db_connection() date_s = datetime.now().strftime("%Y-%m-%d %H:%M") conn.execute("INSERT INTO faxes_out (id, file_path, status, created_at) VALUES (?, ?, 'Entwurf', ?)", (out_id, safe_name, date_s)) conn.commit() conn.close() logging.info(f"Neues Fax für Ausgang importiert: {safe_name}") except PermissionError: pass except Exception as e: logging.error(f"Outbox Check Fehler: {e}") def _process_outbox_status(self): try: conn = get_db_connection() pending = conn.execute("SELECT id, placetel_id FROM faxes_out WHERE status = 'In Arbeit' AND placetel_id IS NOT NULL").fetchall() if not pending: conn.close() return headers = {"Authorization": f"Bearer {self.api_token}", "Accept": "application/json"} for p in pending: url = f"https://api.placetel.de/v2/faxes/{p['placetel_id']}" try: r = requests.get(url, headers=headers) if r.status_code == 200: data = r.json() api_status = data.get('status', '').lower() new_status = 'In Arbeit' if api_status in ['sent', 'delivered', 'success']: new_status = 'Erfolgreich' elif api_status in ['failed', 'error', 'canceled', 'aborted']: new_status = 'Fehlgeschlagen' if new_status != 'In Arbeit': conn.execute("UPDATE faxes_out SET status = ? WHERE id = ?", (new_status, p['id'])) except Exception as e: logging.error(f"Outbox Status Fetch Error for {p['placetel_id']}: {e}") conn.commit() conn.close() except Exception as e: logging.error(f"Outbox Sync Error: {e}") def _cleanup(self): if self.retention <= 0: return try: cutoff = (datetime.now() - timedelta(days=self.retention)).strftime("%Y-%m-%d %H:%M") conn = get_db_connection() archived_vms = conn.execute("SELECT id, audio_path FROM voicemails WHERE received_at < ? AND is_archived = 1", (cutoff,)).fetchall() for row in archived_vms: if row['audio_path']: p = os.path.join(DOWNLOAD_DIR, row['audio_path']) if os.path.exists(p): try: os.remove(p) except: pass conn.execute("DELETE FROM voicemails WHERE id = ?", (row['id'],)) out_rows = conn.execute("SELECT id, file_path FROM faxes_out WHERE created_at < ? AND status IN ('Gesendet', 'Erfolgreich', 'Fehlgeschlagen')", (cutoff,)).fetchall() for row in out_rows: if row['file_path']: p = os.path.join(DOWNLOAD_DIR, row['file_path']) if os.path.exists(p): try: os.remove(p) except: pass conn.execute("DELETE FROM faxes_out WHERE id = ?", (row['id'],)) conn.commit() conn.close() except Exception as e: logging.error(f"Cleanup Fehler: {e}") def _download_file(self, url, local_path): try: logging.info(f"Versuche Download von URL: {url}") headers = {"User-Agent": "Mozilla/5.0"} r = requests.get(url, stream=True, headers=headers, timeout=10) if r.status_code == 200: with open(local_path, 'wb') as f: for chunk in r.iter_content(1024): f.write(chunk) return True else: logging.error(f"Download fehlgeschlagen (Status {r.status_code}) für URL: {url}") return False except Exception as e: logging.error(f"Download blockiert für URL {url}: {e}") return False def _process_contacts(self): try: url = "https://api.placetel.de/v2/contacts" headers = {"Authorization": f"Bearer {self.api_token}", "Accept": "application/json"} resp = requests.get(url, headers=headers, params={"per_page": 100}) if resp.status_code == 200: contacts = resp.json() if isinstance(contacts, dict): contacts = contacts.get('data', []) conn = get_db_connection() local_data = conn.execute("SELECT id, patient_id FROM contacts WHERE patient_id IS NOT NULL").fetchall() pat_map = {row['id']: row['patient_id'] for row in local_data} conn.execute("DELETE FROM contacts WHERE id NOT LIKE 'gdt_%'") for c in contacts: c_id = str(c.get('id')) fname = c.get('first_name', '') lname = c.get('last_name', '') company = c.get('company', '') number = c.get('phone_work') or c.get('mobile_work') or c.get('phone_home') or '' fax_number = c.get('fax') or c.get('fax_work') or '' p_id = pat_map.get(c_id, None) if not number and not fax_number: continue exists = conn.execute("SELECT 1 FROM contacts WHERE id = ?", (c_id,)).fetchone() if exists: conn.execute("UPDATE contacts SET first_name=?, last_name=?, number=?, company=?, fax_number=? WHERE id=?", (fname, lname, number, company, fax_number, c_id)) else: conn.execute("INSERT INTO contacts (id, first_name, last_name, number, company, fax_number, patient_id) VALUES (?, ?, ?, ?, ?, ?, ?)", (c_id, fname, lname, number, company, fax_number, p_id)) conn.commit() conn.close() except Exception as e: logging.error(f"Kontakt Sync Fehler: {e}") def _process_unified_sync(self): if not self.sync_lock.acquire(blocking=False): logging.info("API Sync wird übersprungen (Prozess läuft bereits).") return try: headers = {"Authorization": f"Bearer {self.api_token}", "Accept": "application/json"} date_from = (datetime.now() - timedelta(days=90)).strftime("%Y-%m-%d") new_stuff = 0 # CALLS if self.module_voicemail: call_url = "https://api.placetel.de/v2/calls" resp_calls = requests.get(call_url, headers=headers, params={"date_from": date_from, "per_page": 100, "order": "desc"}) if resp_calls.status_code == 200: call_items = resp_calls.json() if isinstance(call_items, dict): call_items = call_items.get('data', []) for item in call_items: c_id = str(item.get('id')) c_type = str(item.get('type')).lower() file_url = item.get('file_url') or item.get('recording_url') or item.get('pdf_file_url') if not file_url: continue if c_type == 'voicemail': conn = get_db_connection() exists = conn.execute("SELECT 1 FROM voicemails WHERE id = ?", (c_id,)).fetchone() conn.close() if exists: continue date_s = item.get('created_at', datetime.now().strftime("%Y-%m-%d %H:%M")) sender = str(item.get('from', 'Unbekannt')).replace("sip:", "").split("@")[0].replace('"', '') if sender == 'Anonymous': sender = str(item.get('peer', 'Unbekannt')).replace("sip:", "").split("@")[0].replace('"', '') fname = f"vm_{c_id}.mp3" lpath = os.path.join(DOWNLOAD_DIR, fname) if self._download_file(file_url, lpath): txt_status = "[Transkription wird vorbereitet...]" if self.module_transcription else "[Transkription deaktiviert]" conn = get_db_connection() conn.execute("INSERT INTO voicemails (id, audio_path, transcription, received_at, sender) VALUES (?, ?, ?, ?, ?)", (c_id, fname, txt_status, date_s, sender)) conn.commit() conn.close() new_stuff += 1 # FAXES if self.module_fax: fax_url = "https://api.placetel.de/v2/faxes" resp_fax = requests.get(fax_url, headers=headers, params={"date_from": date_from, "per_page": 50, "order": "desc"}) if resp_fax.status_code == 200: fax_items = resp_fax.json() if isinstance(fax_items, dict): fax_items = fax_items.get('data', []) for item in fax_items: f_id = str(item.get('id')) f_type = str(item.get('type')).lower() if 'in' not in f_type: continue conn = get_db_connection() exists = conn.execute("SELECT 1 FROM faxes WHERE id = ?", (f_id,)).fetchone() conn.close() if exists: continue pdf_url = item.get('file') or item.get('pdf_file_url') or item.get('file_url') if not pdf_url: try: det_r = requests.get(f"{fax_url}/{f_id}", headers=headers) if det_r.status_code == 200: det_data = det_r.json() pdf_url = det_data.get('file') or det_data.get('pdf_file_url') except: pass if not pdf_url: continue sender = item.get('from_number') or item.get('sender_number') or 'Unbekannt' date_s = item.get('created_at', datetime.now().strftime("%Y-%m-%d %H:%M")) fname = f"fax_{f_id}.pdf" lpath = os.path.join(DOWNLOAD_DIR, fname) if self._download_file(pdf_url, lpath): # Titel dynamisch generieren doc_title = extract_pdf_title(lpath, sender) conn = get_db_connection() conn.execute("INSERT INTO faxes (id, sender, file_path, status, received_at, source, title) VALUES (?, ?, ?, 'Neu', ?, 'api', ?)", (f_id, sender, fname, date_s, doc_title)) conn.commit() conn.close() new_stuff += 1 if new_stuff > 0: logging.info(f"Sync abgeschlossen: {new_stuff} neue Elemente.") except Exception as e: logging.error(f"Sync Exception: {e}") finally: self.sync_lock.release() worker = BackgroundWorker() # --- ROUTEN --- @app.route('/') def index(): open_settings = request.args.get('open_settings') == 'true' if config.get('module_voicemail', 'true') == 'true': return redirect(url_for('voicemail_inbox', open_settings=open_settings)) elif config.get('module_fax', 'true') == 'true': return redirect(url_for('fax_inbox', open_settings=open_settings)) elif config.get('module_docs', 'true') == 'true': return redirect(url_for('doc_inbox', open_settings=open_settings)) else: return redirect(url_for('contact_page', open_settings=open_settings)) # --- AUTH --- @app.route('/login', methods=['POST']) def login(): username = request.form.get('username') password = request.form.get('password') admin_pw = config.get("admin_password", "6hJUE#MM") if username == "admin" and password == admin_pw: session['authenticated'] = True session['user'] = "admin" flash("Willkommen in COM|hub, Admin!", "success") return redirect(url_for('index', open_settings='true')) else: flash("Zugriff verweigert.", "danger") return redirect(request.referrer or url_for('index')) @app.route('/logout') def logout(): session.pop('authenticated', None) session.pop('user', None) session.pop('review_unlocked', None) flash("Sitzung beendet.", "info") return redirect(url_for('index')) # --- CONTENT --- @app.route('/voicemails/inbox') def voicemail_inbox(): conn = get_db_connection() voicemails = conn.execute("SELECT * FROM voicemails WHERE is_archived = 0 ORDER BY received_at DESC").fetchall() contacts = conn.execute("SELECT * FROM contacts ORDER BY last_name, first_name").fetchall() conn.close() lookup = get_contact_lookup() vm_list = [] for vm in voicemails: v = dict(vm) v['received_formatted'] = format_german_date(v.get('received_at', '')) sender_raw = v.get('sender', '') v['resolved_name'] = sender_raw if sender_raw: clean = normalize_number(sender_raw) if clean in lookup: v['resolved_name'] = lookup[clean] vm_list.append(v) return render_template('index.html', active_tab="vm_inbox", voicemails=vm_list, contacts=contacts, config=config, open_settings=request.args.get('open_settings')) @app.route('/voicemails/archive') def voicemail_archive(): conn = get_db_connection() voicemails = conn.execute("SELECT * FROM voicemails WHERE is_archived = 1 ORDER BY received_at DESC").fetchall() contacts = conn.execute("SELECT * FROM contacts ORDER BY last_name, first_name").fetchall() conn.close() lookup = get_contact_lookup() vm_list = [] for vm in voicemails: v = dict(vm) v['received_formatted'] = format_german_date(v.get('received_at', '')) sender_raw = v.get('sender', '') v['resolved_name'] = sender_raw if sender_raw: clean = normalize_number(sender_raw) if clean in lookup: v['resolved_name'] = lookup[clean] vm_list.append(v) return render_template('index.html', active_tab="vm_archive", voicemails=vm_list, contacts=contacts, config=config, open_settings=request.args.get('open_settings')) @app.route('/voicemails/retranscribe/') def retranscribe_voicemail(v_id): if config.get('allow_retranscription', 'false') != 'true' or config.get('module_transcription', 'true') != 'true': flash("Transkription ist deaktiviert.", "danger") return redirect(request.referrer) conn = get_db_connection() conn.execute("UPDATE voicemails SET transcription = '[Transkription wird vorbereitet...]' WHERE id = ?", (v_id,)) conn.commit() conn.close() flash("Neu-Transkription eingereiht. Das Ergebnis erscheint in Kürze.", "info") return redirect(request.referrer) @app.route('/faxes/inbox') def fax_inbox(): conn = get_db_connection() faxes = conn.execute("SELECT * FROM faxes WHERE status = 'Neu' AND IFNULL(source, 'api') = 'api' ORDER BY received_at DESC").fetchall() contacts = conn.execute("SELECT * FROM contacts ORDER BY last_name, first_name").fetchall() conn.close() faxes_resolved = resolve_faxes_with_names(faxes) return render_template('index.html', active_tab="fax_inbox", faxes=faxes_resolved, contacts=contacts, config=config, open_settings=request.args.get('open_settings')) @app.route('/documents/inbox') def doc_inbox(): conn = get_db_connection() faxes = conn.execute("SELECT * FROM faxes WHERE status = 'Neu' AND source = 'folder' ORDER BY received_at DESC").fetchall() contacts = conn.execute("SELECT * FROM contacts ORDER BY last_name, first_name").fetchall() conn.close() faxes_resolved = resolve_faxes_with_names(faxes) return render_template('index.html', active_tab="doc_inbox", faxes=faxes_resolved, contacts=contacts, config=config, open_settings=request.args.get('open_settings')) @app.route('/faxes/review') def fax_review(): conn = get_db_connection() faxes = conn.execute("SELECT * FROM faxes WHERE status = 'Prüfung' ORDER BY received_at DESC").fetchall() contacts = conn.execute("SELECT * FROM contacts ORDER BY last_name, first_name").fetchall() conn.close() faxes_resolved = resolve_faxes_with_names(faxes) is_locked = False if str(config.get('review_pin_enabled', 'false')).lower() == 'true' and not session.get('review_unlocked'): is_locked = True return render_template('index.html', active_tab="fax_review", faxes=faxes_resolved, contacts=contacts, config=config, open_settings=request.args.get('open_settings'), is_locked=is_locked) @app.route('/faxes/outbox') def fax_outbox(): conn = get_db_connection() faxes_out = conn.execute("SELECT * FROM faxes_out ORDER BY created_at DESC").fetchall() contacts = conn.execute("SELECT * FROM contacts ORDER BY last_name, first_name").fetchall() conn.close() faxes_out_resolved = resolve_outbox_with_names(faxes_out) return render_template('index.html', active_tab="fax_outbox", faxes_out=faxes_out_resolved, contacts=contacts, config=config, open_settings=request.args.get('open_settings')) @app.route('/faxes/review/unlock', methods=['POST']) def unlock_review(): pin = request.form.get('pin') if pin == config.get('review_pin', '0000'): session['review_unlocked'] = True flash("Prüfliste erfolgreich entsperrt.", "success") else: flash("Falscher PIN!", "danger") return redirect(url_for('fax_review')) @app.route('/faxes/review/lock') def lock_review(): session.pop('review_unlocked', None) flash("Prüfliste wurde wieder gesperrt.", "info") return redirect(url_for('fax_review')) # --- ADRESSBUCH --- @app.route('/contacts') def contact_page(): conn = get_db_connection() contacts = conn.execute("SELECT * FROM contacts ORDER BY last_name, first_name").fetchall() conn.close() return render_template('index.html', active_tab="contacts", contacts=contacts, config=config, open_settings=request.args.get('open_settings')) @app.route('/contacts/add', methods=['POST']) def contact_add(): first_name = request.form.get('first_name') last_name = request.form.get('last_name') company = request.form.get('company') number = request.form.get('number', '') fax_number = request.form.get('fax_number', '') patient_id = request.form.get('patient_id', '') if not number and not fax_number and not patient_id: flash("Rufnummer, Faxnummer oder Patienten-ID ist Pflicht!", "danger") return redirect(url_for('contact_page')) try: url = "https://api.placetel.de/v2/contacts" headers = {"Authorization": f"Bearer {config.get('placetel_api_token')}", "Content-Type": "application/json"} payload = {"first_name": first_name, "last_name": last_name, "company": company, "phone_work": number, "fax": fax_number} resp = requests.post(url, headers=headers, json=payload) if resp.status_code == 200 or resp.status_code == 201: if patient_id: data = resp.json() c_id = str(data.get('id', '')) conn = get_db_connection() conn.execute("UPDATE contacts SET patient_id = ? WHERE id = ?", (patient_id, c_id)) conn.commit() conn.close() flash("Kontakt erstellt.", "success") threading.Thread(target=worker._process_contacts).start() else: flash(f"API Fehler: {resp.text}", "danger") except Exception as e: flash(f"Fehler: {e}", "danger") return redirect(url_for('contact_page')) @app.route('/contacts/edit', methods=['POST']) def contact_edit(): c_id = request.form.get('id') first_name = request.form.get('first_name') last_name = request.form.get('last_name') company = request.form.get('company') number = request.form.get('number', '') fax_number = request.form.get('fax_number', '') patient_id = request.form.get('patient_id', '') if not c_id: flash("Fehler: ID fehlt.", "danger") return redirect(url_for('contact_page')) try: if not str(c_id).startswith('gdt_'): # API Kontakt url = f"https://api.placetel.de/v2/contacts/{c_id}" headers = {"Authorization": f"Bearer {config.get('placetel_api_token')}", "Content-Type": "application/json"} payload = {"first_name": first_name, "last_name": last_name, "company": company, "phone_work": number, "fax": fax_number} resp = requests.put(url, headers=headers, json=payload) if resp.status_code == 200: conn = get_db_connection() conn.execute("UPDATE contacts SET patient_id = ? WHERE id = ?", (patient_id, c_id)) conn.commit() conn.close() flash("Kontakt aktualisiert.", "success") threading.Thread(target=worker._process_contacts).start() else: flash(f"Fehler beim Update: {resp.text}", "danger") else: # Lokaler Kontakt conn = get_db_connection() conn.execute("UPDATE contacts SET first_name=?, last_name=?, company=?, number=?, fax_number=?, patient_id=? WHERE id=?", (first_name, last_name, company, number, fax_number, patient_id, c_id)) conn.commit() conn.close() flash("Lokaler Kontakt aktualisiert.", "success") except Exception as e: flash(f"Systemfehler: {e}", "danger") return redirect(url_for('contact_page')) @app.route('/contacts/delete', methods=['POST']) def contact_delete(): c_id = request.form.get('id') pin = request.form.get('pin') if pin != config.get("admin_pin", "1234"): flash("Falscher PIN!", "danger") return redirect(url_for('contact_page')) try: if not str(c_id).startswith('gdt_'): # API Kontakt url = f"https://api.placetel.de/v2/contacts/{c_id}" headers = {"Authorization": f"Bearer {config.get('placetel_api_token')}"} resp = requests.delete(url, headers=headers) if resp.status_code == 200 or resp.status_code == 204: conn = get_db_connection() conn.execute("DELETE FROM contacts WHERE id = ?", (c_id,)) conn.commit() conn.close() flash("Kontakt gelöscht.", "success") else: flash(f"Löschen fehlgeschlagen: {resp.text}", "danger") else: # Lokaler Kontakt conn = get_db_connection() conn.execute("DELETE FROM contacts WHERE id = ?", (c_id,)) conn.commit() conn.close() flash("Lokaler Kontakt gelöscht.", "success") except Exception as e: flash(f"Fehler: {e}", "danger") return redirect(url_for('contact_page')) @app.route('/api/live_queue') def api_live_queue(): api_token = config.get("placetel_api_token") if not api_token or config.get("module_voicemail", "true") != "true": return {"calls": []} try: url = "https://api.placetel.de/v2/call_center_calls" headers = {"Authorization": f"Bearer {api_token}", "Accept": "application/json"} resp = requests.get(url, headers=headers, timeout=5) if resp.status_code != 200: return {"calls": []} calls = resp.json() if isinstance(calls, dict): calls = calls.get('data', []) lookup = get_contact_lookup() live_calls = [] for c in calls: caller = c.get('from', c.get('caller_id', 'Unbekannt')) clean_caller = normalize_number(caller) name = caller if clean_caller in lookup: name = lookup[clean_caller] else: for num, cname in lookup.items(): if len(num) > 6 and (clean_caller.endswith(num) or num.endswith(clean_caller)): name = cname break wait_time_sec = 0 created_at = c.get('created_at') if created_at: try: dt_str = created_at.replace('Z', '').split('+')[0] if '.' in dt_str: dt_str = dt_str.split('.')[0] dt_obj = datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S") wait_time_sec = int((datetime.utcnow() - dt_obj).total_seconds()) if wait_time_sec < 0: wait_time_sec = 0 except: pass live_calls.append({"id": c.get('id', str(time.time())), "caller": caller, "name": name, "wait_time": wait_time_sec}) return {"calls": live_calls} except Exception as e: logging.error(f"Live Queue Error: {e}") return {"calls": []} @app.route('/api/counts') def api_counts(): counts = dict(cnt_vm=0, cnt_fax=0, cnt_rev=0, cnt_out=0, cnt_doc=0) try: conn = get_db_connection() counts['cnt_vm'] = conn.execute("SELECT count(*) FROM voicemails WHERE status='Neu' AND is_archived=0").fetchone()[0] counts['cnt_fax'] = conn.execute("SELECT count(*) FROM faxes WHERE status='Neu' AND IFNULL(source, 'api') = 'api'").fetchone()[0] counts['cnt_doc'] = conn.execute("SELECT count(*) FROM faxes WHERE status='Neu' AND source = 'folder'").fetchone()[0] counts['cnt_rev'] = conn.execute("SELECT count(*) FROM faxes WHERE status='Prüfung'").fetchone()[0] counts['cnt_out'] = conn.execute("SELECT count(*) FROM faxes_out WHERE status='Entwurf'").fetchone()[0] conn.close() except: pass return counts # --- ACTIONS --- @app.route('/fax/action//') def fax_action(f_id, action): conn = get_db_connection() fax = conn.execute("SELECT * FROM faxes WHERE id = ?", (f_id,)).fetchone() if not fax: conn.close() return "Not found", 404 if action == 'to_review': conn.execute("UPDATE faxes SET status = 'Prüfung' WHERE id = ?", (f_id,)) conn.commit() flash("Dokument zur Prüfliste verschoben.", "info") conn.close() return redirect(request.referrer) @app.route('/fax/approve_export', methods=['POST']) def fax_approve_export(): f_id = request.form.get('fax_id') raw_name = request.form.get('filename', '').strip() conn = get_db_connection() fax = conn.execute("SELECT * FROM faxes WHERE id = ?", (f_id,)).fetchone() if not fax: conn.close() flash("Dokument nicht gefunden.", "danger") return redirect(request.referrer) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") safe_sender = "".join(x for x in str(fax['sender']) if x.isalnum() or x in ['.', '-']) if raw_name: safe_name = re.sub(r'[^a-zA-Z0-9_\-äöüÄÖÜß ]', '_', raw_name).strip('_') if not safe_name: safe_name = f"{timestamp}_{safe_sender}" else: safe_name = f"{timestamp}_{safe_sender}" if not safe_name.lower().endswith(".pdf"): safe_name += ".pdf" export_path = config.get("standard_export_path", "/opt/comhub/share/Export") try: os.makedirs(export_path, exist_ok=True) source_file = os.path.join(DOWNLOAD_DIR, fax['file_path']) dest_path = os.path.join(export_path, safe_name) if os.path.exists(source_file): if os.path.exists(dest_path): safe_name = f"{timestamp}_{safe_name}" dest_path = os.path.join(export_path, safe_name) shutil.move(source_file, dest_path) conn.execute("UPDATE faxes SET status = 'Freigegeben' WHERE id = ?", (f_id,)) conn.commit() flash(f"Erfolgreich exportiert: {safe_name}", "success") else: flash("Originaldatei nicht mehr vorhanden.", "danger") except Exception as e: flash(f"Fehler: {e}", "danger") conn.close() return redirect(request.referrer) @app.route('/fax/export_gdt', methods=['POST']) def fax_export_gdt(): f_id = request.form.get('fax_id') patient_id = request.form.get('patient_id') custom_filename = request.form.get('custom_filename', '').strip() if not f_id or not patient_id: flash("Fehlende Daten für GDT Export.", "danger") return redirect(request.referrer) conn = get_db_connection() fax = conn.execute("SELECT * FROM faxes WHERE id = ?", (f_id,)).fetchone() if not fax: conn.close() flash("Dokument nicht gefunden.", "danger") return redirect(request.referrer) gdt_out_dir = config.get("gdt_export_path", "/opt/comhub/share/GDT/Export") try: os.makedirs(gdt_out_dir, exist_ok=True) source_file = os.path.join(DOWNLOAD_DIR, fax['file_path']) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") document_title = "Eingehendes Dokument" if custom_filename: document_title = custom_filename safe_custom = re.sub(r'[^a-zA-Z0-9_\-]', '_', custom_filename).strip('_') if not safe_custom: safe_custom = f"{timestamp}_Pat_{patient_id}" safe_name = f"{safe_custom}.pdf" else: safe_name = f"{timestamp}_Pat_{patient_id}.pdf" dest_path = os.path.join(gdt_out_dir, safe_name) if os.path.exists(dest_path): safe_name = f"{timestamp}_{safe_name}" dest_path = os.path.join(gdt_out_dir, safe_name) absolute_pdf_path = os.path.abspath(dest_path) if os.path.exists(source_file): shutil.move(source_file, dest_path) def make_gdt_line(field, val): val_str = str(val) line_content = f"{field}{val_str}\r\n" byte_len = len(line_content.encode('cp437')) + 3 return f"{byte_len:03d}{line_content}" base_lines = [ make_gdt_line("8000", "6310"), make_gdt_line("8315", config.get("gdt_receiver_id", "PVS")), make_gdt_line("8316", config.get("gdt_sender_id", "COMHUB")), make_gdt_line("3000", patient_id), make_gdt_line("6200", datetime.now().strftime("%d%m%Y")), make_gdt_line("6302", safe_name), make_gdt_line("6303", "pdf"), make_gdt_line("6304", document_title), make_gdt_line("6305", absolute_pdf_path) ] total_bytes = sum(len(l.encode('cp437')) for l in base_lines) + 14 lines = [ base_lines[0], make_gdt_line("8100", f"{total_bytes:05d}") ] + base_lines[1:] gdt_filename = os.path.join(gdt_out_dir, f"export_{timestamp}.gdt") with open(gdt_filename, 'w', encoding='cp437') as f: f.writelines(lines) conn.execute("UPDATE faxes SET status = 'Freigegeben' WHERE id = ?", (f_id,)) conn.commit() flash(f"PVS Export erfolgreich. Dateien im Pfad: {gdt_out_dir}", "success") else: flash("Quelldatei nicht mehr vorhanden.", "danger") except Exception as e: flash(f"GDT Export Fehler: {e}", "danger") conn.close() return redirect(request.referrer) @app.route('/fax/save_note', methods=['POST']) def fax_save_note(): out_id = request.form.get('out_id') note = request.form.get('note', '').strip() if out_id: conn = get_db_connection() conn.execute("UPDATE faxes_out SET note = ? WHERE id = ?", (note, out_id)) conn.commit() conn.close() return "", 204 @app.route('/fax/send', methods=['POST']) def fax_send(): out_id = request.form.get('out_id') raw_recipient = request.form.get('recipient') note = request.form.get('note', '').strip() recipient = normalize_number(raw_recipient) if not recipient: flash("Gültige Zielnummer erforderlich.", "danger") return redirect(request.referrer) from_number = config.get('fax_from_number', '') email = config.get('fax_email', '') if not email: flash("Senden fehlgeschlagen: Absender E-Mail fehlt. Bitte in den Einstellungen eintragen.", "danger") return redirect(request.referrer) conn = get_db_connection() fax = conn.execute("SELECT * FROM faxes_out WHERE id = ?", (out_id,)).fetchone() if not fax: conn.close() flash("Entwurf nicht gefunden.", "danger") return redirect(request.referrer) file_path = os.path.join(DOWNLOAD_DIR, fax['file_path']) if not os.path.exists(file_path): conn.close() flash("PDF-Datei nicht gefunden.", "danger") return redirect(request.referrer) if os.path.getsize(file_path) == 0: conn.close() flash("Senden fehlgeschlagen: Die PDF-Datei hat 0 Bytes (möglicherweise fehlerhafter Export).", "danger") return redirect(request.referrer) try: with open(file_path, 'rb') as check_f: if not check_f.read(5).startswith(b'%PDF-'): conn.close() flash("Senden abgebrochen: Dieser Entwurf ist defekt/unvollständig. Bitte löschen und Dokument neu drucken.", "danger") return redirect(request.referrer) except Exception: pass try: url = "https://api.placetel.de/v2/faxes" headers = { "Authorization": f"Bearer {config.get('placetel_api_token')}", "Content-Type": "application/json", "Accept": "application/json" } with open(file_path, 'rb') as f: pdf_b64 = base64.b64encode(f.read()).decode('utf-8') payload = { 'to_number': recipient, 'email': email, 'file': f"data:application/pdf;base64,{pdf_b64}" } if from_number: payload['from_number'] = from_number resp = requests.post(url, headers=headers, json=payload) if resp.status_code in [200, 201]: data = resp.json() placetel_id = str(data.get('id', '')) conn.execute("UPDATE faxes_out SET status = 'In Arbeit', recipient = ?, note = ?, placetel_id = ? WHERE id = ?", (raw_recipient, note, placetel_id, out_id)) conn.commit() flash("Fax erfolgreich an den Anbieter übergeben (Status: In Arbeit).", "success") else: flash(f"Fehler beim Senden: {resp.text}", "danger") except Exception as e: flash(f"Systemfehler: {e}", "danger") conn.close() return redirect(request.referrer) @app.route('/fax/delete', methods=['POST']) def fax_delete(): f_id = request.form.get('fax_id') out_id = request.form.get('out_id') pin = request.form.get('pin') if pin != config.get("admin_pin", "1234"): flash("Falscher PIN!", "danger") return redirect(request.referrer) conn = get_db_connection() if f_id: fax = conn.execute("SELECT * FROM faxes WHERE id = ?", (f_id,)).fetchone() if fax: p = os.path.join(DOWNLOAD_DIR, fax['file_path']) if os.path.exists(p): try: os.remove(p) except: pass conn.execute("DELETE FROM faxes WHERE id = ?", (f_id,)) conn.commit() flash("Eingehendes Dokument gelöscht.", "success") elif out_id: fax_out = conn.execute("SELECT * FROM faxes_out WHERE id = ?", (out_id,)).fetchone() if fax_out: p = os.path.join(DOWNLOAD_DIR, fax_out['file_path']) if os.path.exists(p): try: os.remove(p) except: pass conn.execute("DELETE FROM faxes_out WHERE id = ?", (out_id,)) conn.commit() flash("Ausgehendes Dokument gelöscht.", "success") conn.close() return redirect(request.referrer) @app.route('/sync') def trigger_sync(): worker.sync_now() flash("Abruf gestartet.", "info") return redirect(request.referrer) @app.route('/system/update', methods=['POST']) def system_update(): if session.get('user') != 'admin': flash("Keine Berechtigung.", "danger") return redirect(request.referrer) app_file = request.files.get('app_file') index_file = request.files.get('index_file') needs_restart = False if index_file and index_file.filename: idx_path = os.path.join(TEMPLATES_DIR, 'index.html') if os.path.exists(idx_path): shutil.copy2(idx_path, idx_path + '.bak') index_file.save(idx_path) flash("Weboberfläche (index.html) aktualisiert.", "success") if app_file and app_file.filename: temp_path = os.path.join(BASE_DIR, 'temp_app.py') app_file.save(temp_path) try: py_compile.compile(temp_path, doraise=True) app_path = os.path.join(BASE_DIR, 'app.py') if os.path.exists(app_path): shutil.copy2(app_path, app_path + '.bak') shutil.move(temp_path, app_path) needs_restart = True flash("Backend (app.py) aktualisiert. System führt Neustart durch...", "success") except Exception as e: os.remove(temp_path) flash(f"Update abgebrochen! Syntax-Fehler in der Datei: {e}", "danger") return redirect(request.referrer) session.pop('authenticated', None) session.pop('user', None) if needs_restart: def restart_service(): time.sleep(2) subprocess.Popen(["systemctl", "restart", "comhub"]) threading.Thread(target=restart_service, daemon=True).start() return redirect(request.referrer) @app.route('/settings', methods=['POST']) def settings(): if not session.get('authenticated'): flash("Nicht eingeloggt!", "danger") return redirect(request.referrer) try: config['placetel_api_token'] = request.form.get('api_token', '').strip() config['check_interval_minutes'] = int(request.form.get('interval', 10)) days = int(request.form.get('days', 7)) config['delete_recordings_after_days'] = max(1, min(7, days)) config['standard_export_path'] = request.form.get('standard_export_path', '/opt/comhub/share/Export').strip() config['fax_outbox_path'] = request.form.get('fax_outbox_path', '/opt/comhub/share/FaxOut').strip() config['fax_from_number'] = request.form.get('fax_from_number', '').strip() config['fax_email'] = request.form.get('fax_email', '').strip() config['admin_pin'] = request.form.get('admin_pin', '1234').strip() config['module_voicemail'] = 'true' if request.form.get('module_voicemail') else 'false' config['module_transcription'] = 'true' if request.form.get('module_transcription') else 'false' config['module_fax'] = 'true' if request.form.get('module_fax') else 'false' config['module_gdt'] = 'true' if request.form.get('module_gdt') else 'false' config['module_docs'] = 'true' if request.form.get('module_docs') else 'false' config['review_pin_enabled'] = 'true' if request.form.get('review_pin_enabled') else 'false' config['review_pin'] = request.form.get('review_pin', '0000').strip() config['gdt_import_path'] = request.form.get('gdt_import_path', '/opt/comhub/share/GDT/Import').strip() config['gdt_export_path'] = request.form.get('gdt_export_path', '/opt/comhub/share/GDT/Export').strip() config['gdt_sender_id'] = request.form.get('gdt_sender_id', 'COMHUB').strip() config['gdt_receiver_id'] = request.form.get('gdt_receiver_id', 'PVS').strip() config['monitored_folder_path'] = request.form.get('monitored_folder_path', '/opt/comhub/share/Dokumente/Import').strip() config['allow_retranscription'] = 'true' if request.form.get('allow_retranscription') else 'false' new_admin_pw = request.form.get('admin_password', '').strip() if new_admin_pw: config['admin_password'] = new_admin_pw save_config(config) worker.update_config() session.pop('authenticated', None) session.pop('user', None) flash("Einstellungen gespeichert. Sie wurden sicherheitshalber abgemeldet.", "success") except Exception as e: flash(f"Fehler: {e}", "danger") return redirect(request.referrer) @app.route('/status//') def set_status(v_id, new_status): conn = get_db_connection() if new_status == 'Erledigt': conn.execute("UPDATE voicemails SET status = 'Erledigt', is_archived = 1 WHERE id = ?", (v_id,)) elif new_status == 'Neu': conn.execute("UPDATE voicemails SET status = 'Neu', is_archived = 0 WHERE id = ?", (v_id,)) conn.commit() conn.close() return redirect(request.referrer) @app.route('/archive_toggle/') def archive_toggle(v_id): conn = get_db_connection() conn.execute("UPDATE voicemails SET is_archived = NOT is_archived WHERE id = ?", (v_id,)) conn.commit() conn.close() return redirect(request.referrer) @app.route('/files/') def serve_file(filename): return send_from_directory(DOWNLOAD_DIR, filename) if __name__ == '__main__': worker.start_scheduler() print("Server läuft auf http://0.0.0.0:5000") app.run(debug=True, use_reloader=False, host='0.0.0.0', port=5000)