diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py
index 83bbd50..2caf102 100644
--- a/ui/tab_a_logic.py
+++ b/ui/tab_a_logic.py
@@ -12,6 +12,7 @@ import os
import datetime
import json
import tempfile
+import html
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorFileWriter,
@@ -63,9 +64,66 @@ class TabALogic:
self.link_pruefer = link_pruefer
self.stil_pruefer = stil_pruefer
self.data_grabber: Optional[DataGrabber] = None
+
+ class _SilentPruefmanagerProxy:
+ """Proxy, der Prüfergebnisse ohne UI-Meldungen verarbeitet."""
+
+ def __init__(self, outer: "TabALogic", fallback: Optional[Any]) -> None:
+ self._outer = outer
+ self._fallback = fallback
+ self.results: List[Any] = []
+
+ def verarbeite(self, ergebnis: Any) -> Any:
+ self.results.append(ergebnis)
+ ok = bool(getattr(ergebnis, "ok", False))
+ if not ok:
+ aktion = getattr(ergebnis, "aktion", "unbekannt")
+ meldung = getattr(ergebnis, "meldung", "")
+ self._outer._log(f"[SILENT-PRUEFMANAGER] {aktion}: {meldung}")
+ return ergebnis
+
+ def __getattr__(self, name: str) -> Any:
+ if self._fallback is not None:
+ return getattr(self._fallback, name)
+ raise AttributeError(name)
+
def _log(self, msg: str) -> None:
print(f"[TabALogic] {msg}")
+ def _create_silent_pruefmanager(self) -> "TabALogic._SilentPruefmanagerProxy":
+ return TabALogic._SilentPruefmanagerProxy(self, self.pruefmanager)
+
+ def _extract_datenabruf_error_maps(self, datenabruf_results: List[Any]) -> Dict[str, Dict[str, str]]:
+ by_ident: Dict[str, str] = {}
+ by_thema: Dict[str, str] = {}
+
+ for result in datenabruf_results or []:
+ if not result:
+ continue
+ context = getattr(result, "kontext", None)
+ if not isinstance(context, dict):
+ continue
+
+ if getattr(result, "aktion", None) == "url_nicht_erreichbar":
+ ident = str(context.get("ident") or "").strip()
+ thema = str(context.get("thema") or "").strip()
+ reason = str(context.get("error") or getattr(result, "meldung", "") or "unbekannter Fehler").strip()
+ if ident and reason:
+ by_ident[ident] = reason
+ if thema and reason:
+ by_thema[thema] = reason
+
+ if getattr(result, "aktion", None) == "datenabruf":
+ fehler_map = context.get("fehler")
+ if isinstance(fehler_map, dict):
+ for thema_key, value in fehler_map.items():
+ thema = str(thema_key or "").strip()
+ reason = str(value or "").strip()
+ if thema and reason and thema not in by_thema:
+ by_thema[thema] = reason
+
+ return {"by_ident": by_ident, "by_thema": by_thema}
+
def _zeige_verfahrensgebiet_hinweis(self) -> None:
if not self.pruefmanager:
return
@@ -254,7 +312,16 @@ class TabALogic:
progress.set_label("Prüfe Eingaben...")
# 5) Daten aus Linkliste laden und prüfen
- source_dict, grabber_summary = self.data_grabber.run(linkliste_final)
+ silent_pruefmanager = self._create_silent_pruefmanager()
+ local_data_grabber = DataGrabber(
+ silent_pruefmanager,
+ datei_pruefer_cls=self.data_grabber._datei_pruefer_cls,
+ link_pruefer=self.data_grabber.link_pruefer,
+ layer_pruefer=self.data_grabber.layer_pruefer,
+ stil_pruefer=self.data_grabber.stil_pruefer,
+ excel_importer_cls=self.data_grabber._excel_importer_cls,
+ )
+ source_dict, grabber_summary = local_data_grabber.run(linkliste_final)
self._log(f"DataGrabber: {grabber_summary.meldung} [{grabber_summary.aktion}]")
# DEBUG: detaillierter Status
@@ -285,7 +352,8 @@ class TabALogic:
self._log("Warnung: DataGrabber meldet fehlerhafte Zeilen, fahre mit Validierungsdaten fort")
# 5) Datenabruf (aus validierten Zeilen)
- datenabruf = Datenabruf(self.pruefmanager)
+ silent_pruefmanager = self._create_silent_pruefmanager()
+ datenabruf = Datenabruf(silent_pruefmanager)
result_dict, datenabruf_results = datenabruf.datenabruf(
result_dict=source_dict,
raumfilter=raumfilter,
@@ -315,7 +383,7 @@ class TabALogic:
progress.set_label("Fachdaten laden abgeschlossen. Bitte OK klicken, um den Dialog zu schließen.")
# 7) Log-Datei schreiben
- self._write_markdown_log(final_pfad, source_dict, pipeline_context, load_summary)
+ self._write_html_log(final_pfad, source_dict, pipeline_context, load_summary)
print("=" * 60 + "\n")
return pipeline_context
@@ -329,7 +397,15 @@ class TabALogic:
aborted = False
if not rows:
self._log("Keine Dienste zum Laden")
- return
+ return {
+ "row_count": 0,
+ "loaded_count": 0,
+ "skipped_outside": 0,
+ "aborted": False,
+ "row_stats": [],
+ "layer_call_status": {},
+ "raumfilter_name": pipeline_context.get("raumfilter_name", "unbekannt"),
+ }
final_pfad = pipeline_context.get("source") or ""
use_datenschreiber = bool(final_pfad)
@@ -359,7 +435,12 @@ class TabALogic:
raumfilter_extent = raumfilter_layer.extent()
temp_layers: List[Any] = []
- layer_loader = LayerLoader(self.pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer)
+ silent_pruefmanager = self._create_silent_pruefmanager()
+ layer_loader = LayerLoader(silent_pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer)
+
+ error_maps = self._extract_datenabruf_error_maps(pipeline_context.get("datenabruf_results", []))
+ error_by_ident = error_maps.get("by_ident", {})
+ error_by_thema = error_maps.get("by_thema", {})
# Statistiken für Log: Raumfilter-Info pro Dienst
row_stats: List[Dict[str, Any]] = []
@@ -376,6 +457,7 @@ class TabALogic:
daten_map = (pipeline_context.get("result_dict") or {}).get("daten", {})
fetched_features = daten_map.get(ident, []) if isinstance(daten_map, dict) else []
fetched_count = len(fetched_features) if isinstance(fetched_features, list) else None
+ base_reason = error_by_ident.get(ident) or error_by_thema.get(thema)
if progress is not None:
progress.set_label(f"Lade Dienst {idx}/{total}: {thema}")
@@ -404,6 +486,8 @@ class TabALogic:
if not layer:
layer_call_status[thema] = "layer_nicht_ladbar"
self._log(f"[DEBUG] Layeraufruf fehlgeschlagen: thema='{thema}'")
+ reason = base_reason or "Layer konnte nicht geladen werden (Dienst nicht erreichbar oder Link fehlerhaft)"
+ self._log(f"[DEBUG] Dienst nicht geladen: {thema} | Grund: {reason}")
row_stats.append({
"dienst": thema,
"provider": provider,
@@ -413,7 +497,7 @@ class TabALogic:
"total_features": None,
"filtered_features": None,
"status": "layer_nicht_ladbar",
- "raumfilter": raumfilter_name,
+ "reason": reason,
})
continue
@@ -467,6 +551,7 @@ class TabALogic:
layer_call_status[thema] = "raumfilter_ausserhalb"
self._log(f"Dienst {thema} ist außerhalb des Raumfilters")
skipped_outside += 1
+ reason = "Keine Objekte innerhalb des Raumfilters"
row_stats.append({
"dienst": thema,
"provider": provider,
@@ -476,7 +561,7 @@ class TabALogic:
"total_features": total_features,
"filtered_features": 0,
"status": "außerhalb",
- "raumfilter": raumfilter_name,
+ "reason": reason,
})
continue
@@ -492,7 +577,7 @@ class TabALogic:
"total_features": total_features,
"filtered_features": filtered_features,
"status": "geladen",
- "raumfilter": raumfilter_name,
+ "reason": "geladen",
})
layer_call_status[thema] = "geladen"
self._log(f"[DEBUG] Dienst geladen: thema='{thema}', provider='{provider}', filtered_features={filtered_features}")
@@ -690,75 +775,118 @@ class TabALogic:
self._log(f"[DEBUG] Fehler beim Erzeugen lokaler Featureschicht für {thema}: {exc}")
return None
- def _write_markdown_log(
+ def _write_html_log(
self,
final_pfad: str,
source_dict: DataDict,
pipeline_context: Dict[str, Any],
load_summary: Dict[str, Any],
) -> None:
- """Schreibt den Pipeline-Log (Markdown)."""
+ """Schreibt den Pipeline-Log als HTML-Datei."""
lines = [
- "# Plan41 Fachdaten-Ladevorgang",
- "",
- f"**Datum**: {datetime.datetime.now().isoformat()}",
- f"**Verfahrens-DB**: {final_pfad or 'temporär'}",
- f"**Linkliste**: {pipeline_context.get('linkliste')}",
- "",
- "## Zusammenfassung",
- "",
- f"- **Zeilen gesamt**: {load_summary.get('row_count', 0)}",
- f"- **Geladene Dienste**: {load_summary.get('loaded_count', 0)}",
- f"- **Außerhalb Raumfilter**: {load_summary.get('skipped_outside', 0)}",
- f"- **Abgebrochen**: {load_summary.get('aborted', False)}",
- f"- **Raumfilter**: {load_summary.get('raumfilter_name', 'unbekannt')}",
- f"- **Raumfilter-Typ**: {pipeline_context.get('raumfilter_name', 'unbekannt')}",
- "",
- "## Dienstliste",
- "",
- "| Dienst | Provider | Linkadresse | Aufrufstatus | Ergebnisstatus |",
- "|---|---|---|---|---|",
+ "",
+ "",
+ "
",
+ " ",
+ " Plan41 Fachdaten-Ladevorgang",
+ " ",
+ "",
+ "",
+ "Plan41 Fachdaten-Ladevorgang
",
+ "",
+ "Zusammenfassung
",
+ "",
+ f" - Zeilen gesamt: {load_summary.get('row_count', 0)}
",
+ f" - Geladene Dienste: {load_summary.get('loaded_count', 0)}
",
+ f" - Außerhalb Raumfilter: {load_summary.get('skipped_outside', 0)}
",
+ f" - Abgebrochen: {load_summary.get('aborted', False)}
",
+ "
",
+ "Dienstliste
",
+ "",
+ "| Dienst | Provider | Linkadresse | Aufrufstatus |
",
+ "",
]
- status_by_dienst = {
- str(stat.get("dienst", "")): str(stat.get("status", "n/a"))
- for stat in load_summary.get("row_stats", [])
- }
aufrufstatus_by_dienst = {
str(key): str(value)
for key, value in (load_summary.get("layer_call_status", {}) or {}).items()
}
+ aufrufstatus_labels = {
+ "layeraufruf_start": "Layer-Aufruf gestartet",
+ "layeraufruf_ok": "Layer-Aufruf erfolgreich",
+ "geladen": "Geladen",
+ "layer_nicht_ladbar": "Nicht ladbar",
+ "raumfilter_ausserhalb": "Außerhalb Raumfilter",
+ "abbruch_vor_layeraufruf": "Abbruch vor Layer-Aufruf",
+ "abbruch_nach_layeraufruf": "Abbruch nach Layer-Aufruf",
+ "abbruch_nach_raumfilter": "Abbruch nach Raumfilter",
+ "nicht_aufgerufen": "Nicht aufgerufen",
+ }
+ status_labels = {
+ "geladen": "Geladen",
+ "außerhalb": "Außerhalb Raumfilter",
+ "layer_nicht_ladbar": "Nicht ladbar",
+ }
for row in source_dict.get('rows', []):
dienst = row.get('Inhalt') or row.get('ident') or ''
provider = row.get('Provider') or ''
link = row.get('Link') or ''
aufrufstatus = aufrufstatus_by_dienst.get(str(dienst), "nicht_aufgerufen")
- ergebnisstatus = status_by_dienst.get(str(dienst), "n/a")
- lines.append(f"| {dienst} | {provider} | {link} | {aufrufstatus} | {ergebnisstatus} |")
+ aufrufstatus_text = aufrufstatus_labels.get(str(aufrufstatus), str(aufrufstatus))
+ lines.append(
+ f"| {html.escape(str(dienst))} | {html.escape(str(provider))} | {html.escape(str(link))} | {html.escape(str(aufrufstatus_text))} |
"
+ )
lines.extend([
- "",
- "## Raumfilter-Statistik",
- "",
- "| Dienst | Provider | Linkadresse | Datenabruf-Objekte | Gesamt-Objekte | Gefilterte Objekte | Raumfilter | Status |",
- "|---|---|---|---|---|---|---|---|",
+ "",
+ "
",
+ "Raumfilter-Statistik
",
+ "",
+ "| Dienst | Datenabruf-Objekte | Gesamt-Objekte | Gefilterte Objekte | Status |
",
+ "",
])
for stat in load_summary.get('row_stats', []):
+ status = str(stat.get('status', 'n/a'))
+ status_label = status_labels.get(status, status)
+ reason = str(stat.get('reason', '') or '')
+ status_text = status_label if not reason else f"{status_label}: {reason}"
lines.append(
- f"| {stat.get('dienst', '')} | {stat.get('provider', '')} | {stat.get('link', '')} | {stat.get('datenabruf_features', 'n/a')} | {stat.get('total_features', 'n/a')} | {stat.get('filtered_features', 'n/a')} | {stat.get('raumfilter', '')} | {stat.get('status', 'n/a')} |"
+ f"| {html.escape(str(stat.get('dienst', '')))} | {html.escape(str(stat.get('datenabruf_features', 'n/a')))} | {html.escape(str(stat.get('total_features', 'n/a')))} | {html.escape(str(stat.get('filtered_features', 'n/a')))} | {html.escape(status_text)} |
"
)
- markdown = "\n".join(lines)
+ lines.extend([
+ "",
+ "
",
+ "",
+ "",
+ ])
+
+ html_text = "\n".join(lines)
if final_pfad:
log_dir = os.path.dirname(final_pfad)
os.makedirs(log_dir, exist_ok=True)
- log_file = os.path.join(log_dir, "plan41_lade_log.md")
+ log_file = os.path.join(log_dir, "plan41_lade_log.html")
try:
with open(log_file, "w", encoding="utf-8") as fh:
- fh.write(markdown)
+ fh.write(html_text)
self.pruefmanager.verarbeite(
pruef_ergebnis(
@@ -773,15 +901,33 @@ class TabALogic:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
- meldung=f"Fehler beim Schreiben des Logle (md): {exc}",
+ meldung=f"Fehler beim Schreiben des Logs (html): {exc}",
aktion="log_schreiben_fehlgeschlagen",
kontext={"error": str(exc)},
)
)
warning("Lade-Log", f"Konnte Datei nicht schreiben: {exc}", duration=10)
else:
- # temporärer Modus: nur anzeigen
- info("Lade-Log (temporär)", markdown, duration=20)
+ try:
+ tmp_dir = os.path.join(tempfile.gettempdir(), "sn_plan41")
+ os.makedirs(tmp_dir, exist_ok=True)
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ log_file = os.path.join(tmp_dir, f"plan41_lade_log_{timestamp}.html")
+
+ with open(log_file, "w", encoding="utf-8") as fh:
+ fh.write(html_text)
+
+ self._log(f"Lade-Log (temporär) gespeichert: {log_file}")
+
+ if hasattr(os, "startfile"):
+ try:
+ os.startfile(log_file)
+ except Exception as exc:
+ self._log(f"Konnte temporäre Log-Datei nicht öffnen: {exc}")
+
+ info("Lade-Log (temporär)", f"Lade-Protokoll gespeichert: {log_file}", duration=20)
+ except Exception as exc:
+ warning("Lade-Log", f"Konnte temporäre Log-Datei nicht schreiben: {exc}", duration=10)
def _clone_layer_with_extent(self, layer: QgsVectorLayer, extent, thema: str) -> QgsVectorLayer | None: