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

", + "
", + f"

Datum: {datetime.datetime.now().isoformat()}

", + f"

Verfahrens-DB: {final_pfad or 'temporär'}

", + f"

Linkliste: {pipeline_context.get('linkliste')}

", + f"

Raumfilter: {load_summary.get('raumfilter_name', 'unbekannt')}

", + "
", + "

Zusammenfassung

", + "", + "

Dienstliste

", + "", + "", + "", ] - 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"" + ) lines.extend([ - "", - "## Raumfilter-Statistik", - "", - "| Dienst | Provider | Linkadresse | Datenabruf-Objekte | Gesamt-Objekte | Gefilterte Objekte | Raumfilter | Status |", - "|---|---|---|---|---|---|---|---|", + "", + "
DienstProviderLinkadresseAufrufstatus
{html.escape(str(dienst))}{html.escape(str(provider))}{html.escape(str(link))}{html.escape(str(aufrufstatus_text))}
", + "

Raumfilter-Statistik

", + "", + "", + "", ]) 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"" ) - markdown = "\n".join(lines) + lines.extend([ + "", + "
DienstDatenabruf-ObjekteGesamt-ObjekteGefilterte ObjekteStatus
{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)}
", + "", + "", + ]) + + 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: