console-error-scanner 1.16.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. console_error_scanner/__init__.py +5 -0
  2. console_error_scanner/__main__.py +370 -0
  3. console_error_scanner/app.py +1870 -0
  4. console_error_scanner/app.tcss +91 -0
  5. console_error_scanner/i18n.py +74 -0
  6. console_error_scanner/locale/de.json +417 -0
  7. console_error_scanner/locale/en.json +417 -0
  8. console_error_scanner/models/__init__.py +1 -0
  9. console_error_scanner/models/history.py +220 -0
  10. console_error_scanner/models/robots.py +154 -0
  11. console_error_scanner/models/scan_result.py +265 -0
  12. console_error_scanner/models/settings.py +242 -0
  13. console_error_scanner/models/sitemap.py +347 -0
  14. console_error_scanner/models/whitelist.py +226 -0
  15. console_error_scanner/screens/__init__.py +1 -0
  16. console_error_scanner/screens/diet_advisor.py +128 -0
  17. console_error_scanner/screens/error_detail.py +164 -0
  18. console_error_scanner/screens/history.py +187 -0
  19. console_error_scanner/screens/scan_summary.py +234 -0
  20. console_error_scanner/screens/settings.py +292 -0
  21. console_error_scanner/screens/top_errors.py +304 -0
  22. console_error_scanner/screens/whitelist.py +154 -0
  23. console_error_scanner/services/__init__.py +1 -0
  24. console_error_scanner/services/image_clipboard.py +104 -0
  25. console_error_scanner/services/preview_service.py +291 -0
  26. console_error_scanner/services/rate_limit.py +56 -0
  27. console_error_scanner/services/reporter.py +561 -0
  28. console_error_scanner/services/scanner.py +965 -0
  29. console_error_scanner/services/site_score.py +151 -0
  30. console_error_scanner/widgets/__init__.py +1 -0
  31. console_error_scanner/widgets/bar_chart.py +60 -0
  32. console_error_scanner/widgets/preview_panel.py +356 -0
  33. console_error_scanner/widgets/results_table.py +594 -0
  34. console_error_scanner/widgets/stats_panel.py +387 -0
  35. console_error_scanner/widgets/summary_panel.py +176 -0
  36. console_error_scanner-1.16.3.dist-info/METADATA +509 -0
  37. console_error_scanner-1.16.3.dist-info/RECORD +41 -0
  38. console_error_scanner-1.16.3.dist-info/WHEEL +5 -0
  39. console_error_scanner-1.16.3.dist-info/entry_points.txt +2 -0
  40. console_error_scanner-1.16.3.dist-info/licenses/LICENSE +202 -0
  41. console_error_scanner-1.16.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """Console Error Scanner - Website-Scanner fuer Console- und HTTP-Fehler."""
2
+
3
+ __version__ = "1.16.2"
4
+ __author__ = "Michael Blaess"
5
+ __year__ = "2026"
@@ -0,0 +1,370 @@
1
+ """Entry Point fuer Console Error Scanner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from typing import TextIO
9
+
10
+ # Frozen-EXE Erkennung (PyInstaller UND Nuitka):
11
+ # PLAYWRIGHT_BROWSERS_PATH muss gesetzt werden BEVOR playwright importiert wird,
12
+ # damit das gebundelte Chromium im "browsers"-Unterordner gefunden wird.
13
+ _is_frozen = getattr(sys, "frozen", False) or "__compiled__" in globals()
14
+ if _is_frozen:
15
+ _exe_dir = os.path.dirname(sys.executable)
16
+ _browsers_dir = os.path.join(_exe_dir, "browsers")
17
+ if os.path.isdir(_browsers_dir):
18
+ os.environ["PLAYWRIGHT_BROWSERS_PATH"] = _browsers_dir
19
+
20
+ from textual_widgets import reset_terminal_title, set_terminal_title
21
+
22
+ # Log-Handle offen halten, solange der Prozess laeuft - faulthandler schreibt
23
+ # beim fatalen Signal direkt hinein. Ohne Referenz wuerde der GC es schliessen.
24
+ _fault_log: TextIO | None = None
25
+
26
+ from console_error_scanner import __version__
27
+ from console_error_scanner.i18n import SUPPORTED_LANGUAGES, load_locale
28
+ from console_error_scanner.models.settings import Settings
29
+
30
+
31
+ def _silence_proactor_teardown_noise() -> None:
32
+ """Unterdrueckt das bekannte Windows-asyncio-Teardown-Rauschen.
33
+
34
+ Auf Windows finalisiert der ProactorEventLoop die Subprocess-/Pipe-
35
+ Transports des Playwright-Chromium teilweise erst per Garbage-Collector
36
+ NACH dem (sauberen) App-Ende. Deren ``__del__`` wirft dann
37
+ 'unclosed transport' (ResourceWarning) bzw. 'I/O operation on closed pipe'
38
+ (ValueError) - reines kosmetisches Rauschen, das ueber ``sys.unraisablehook``
39
+ auf stderr landet. Wir schlucken GENAU diese Faelle; alles andere geht
40
+ unveraendert an den Original-Hook (keine echten Fehler werden versteckt).
41
+ """
42
+ if sys.platform != "win32":
43
+ return
44
+ original = sys.unraisablehook
45
+
46
+ def _hook(unraisable: sys.UnraisableHookArgs) -> None:
47
+ text = f"{unraisable.err_msg or ''} {unraisable.exc_value or ''}"
48
+ if "closed pipe" in text or "unclosed transport" in text:
49
+ return
50
+ original(unraisable)
51
+
52
+ sys.unraisablehook = _hook
53
+
54
+
55
+ def main() -> None:
56
+ """Haupteinstiegspunkt fuer die CLI."""
57
+ _enable_faulthandler()
58
+ _silence_proactor_teardown_noise()
59
+
60
+ settings = Settings.load()
61
+ saved_lang = settings.language
62
+
63
+ parser = argparse.ArgumentParser(
64
+ prog="console-error-scanner",
65
+ description=f"\n Console Error Scanner v{__version__}\n",
66
+ epilog=_usage_examples(),
67
+ formatter_class=argparse.RawDescriptionHelpFormatter,
68
+ )
69
+
70
+ parser.add_argument(
71
+ "sitemap_url",
72
+ nargs="?",
73
+ default="",
74
+ metavar="URL_OR_FILE",
75
+ help=(
76
+ "URL der Website, Sitemap-URL oder lokale sitemap.xml. "
77
+ "Bei Domain-URLs wird die Sitemap automatisch gesucht. "
78
+ "Ohne Argument fragt die App die URL beim ersten 's' ab."
79
+ ),
80
+ )
81
+ parser.add_argument(
82
+ "--concurrency",
83
+ "-c",
84
+ type=int,
85
+ default=None,
86
+ metavar="N",
87
+ help="Max parallele Browser-Tabs (Default aus Settings)",
88
+ )
89
+ parser.add_argument(
90
+ "--timeout",
91
+ "-t",
92
+ type=int,
93
+ default=None,
94
+ metavar="SEC",
95
+ help="Timeout pro Seite in Sekunden (Default aus Settings)",
96
+ )
97
+ parser.add_argument(
98
+ "--output-json",
99
+ default="",
100
+ metavar="PATH",
101
+ help="JSON-Report automatisch speichern",
102
+ )
103
+ parser.add_argument(
104
+ "--output-html",
105
+ default="",
106
+ metavar="PATH",
107
+ help="HTML-Report automatisch speichern",
108
+ )
109
+ parser.add_argument(
110
+ "--no-headless",
111
+ action="store_true",
112
+ default=False,
113
+ help="Browser sichtbar starten (Debugging)",
114
+ )
115
+ parser.add_argument(
116
+ "--filter",
117
+ "-f",
118
+ default="",
119
+ metavar="TEXT",
120
+ help="Nur URLs scannen die TEXT enthalten",
121
+ )
122
+ parser.add_argument(
123
+ "--console-level",
124
+ default="",
125
+ choices=["", "error", "warn", "all"],
126
+ metavar="LEVEL",
127
+ help="Console-Level: error | warn | all (Default aus Settings)",
128
+ )
129
+ parser.add_argument(
130
+ "--user-agent",
131
+ default="",
132
+ metavar="UA",
133
+ help="Custom User-Agent String (Default: realistischer Chrome)",
134
+ )
135
+ parser.add_argument(
136
+ "--cookie",
137
+ action="append",
138
+ default=[],
139
+ metavar="NAME=VALUE",
140
+ help="Cookie setzen (mehrfach moeglich). Default aus Settings.",
141
+ )
142
+ parser.add_argument(
143
+ "--whitelist",
144
+ "-w",
145
+ default="",
146
+ metavar="PATH",
147
+ help="Pfad zur Whitelist-JSON (Default aus Settings)",
148
+ )
149
+ parser.add_argument(
150
+ "--no-consent",
151
+ action="store_true",
152
+ default=None,
153
+ help="Cookie-Consent NICHT akzeptieren (nur Banner verstecken)",
154
+ )
155
+ parser.add_argument(
156
+ "--no-scroll",
157
+ action="store_true",
158
+ default=None,
159
+ help="Seite nicht scrollen (kein Lazy-Loading-Trigger)",
160
+ )
161
+ parser.add_argument(
162
+ "--ignore-robots",
163
+ action="store_true",
164
+ default=None,
165
+ help="robots.txt ignorieren (nur fuer eigene Seiten sinnvoll)",
166
+ )
167
+ parser.add_argument(
168
+ "--lang",
169
+ default=saved_lang,
170
+ choices=SUPPORTED_LANGUAGES,
171
+ help=f"Language ({', '.join(SUPPORTED_LANGUAGES)})",
172
+ )
173
+
174
+ args = parser.parse_args()
175
+
176
+ # Sprache laden (CLI > Settings > Default)
177
+ lang = args.lang
178
+ load_locale(lang)
179
+
180
+ # Sprache persistent speichern wenn per CLI geaendert
181
+ if lang != saved_lang:
182
+ settings.language = lang
183
+ settings.save()
184
+
185
+ # Cookies parsen: "NAME=VALUE" -> {"name": "NAME", "value": "VALUE"}
186
+ cookies = []
187
+ for cookie_str in args.cookie:
188
+ if "=" not in cookie_str:
189
+ from console_error_scanner.i18n import t
190
+
191
+ print(t("cli.invalid_cookie", cookie=cookie_str))
192
+ sys.exit(1)
193
+ name, value = cookie_str.split("=", 1)
194
+ cookies.append({"name": name.strip(), "value": value.strip()})
195
+
196
+ # textual-image (TGP/Sixel) eager initialisieren, sobald das Terminal
197
+ # Grafik-faehig ist - egal ob Vorschau aktuell an oder aus ist. Sonst
198
+ # leaken die DA1-/Cell-Size-Query-Antworten beim spaeteren Aktivieren der
199
+ # Vorschau in den fokussierten Input (Filter-Suche).
200
+ _preinit_graphics_backend()
201
+
202
+ # App NACH load_locale importieren - t() ist sofort verfuegbar
203
+ from console_error_scanner.app import ConsoleErrorScannerApp
204
+
205
+ set_terminal_title(f"✗ console-error-scanner v{__version__}")
206
+ try:
207
+ app = ConsoleErrorScannerApp(
208
+ sitemap_url=args.sitemap_url,
209
+ concurrency=args.concurrency,
210
+ timeout=args.timeout,
211
+ output_json=args.output_json,
212
+ output_html=args.output_html,
213
+ headless=not args.no_headless,
214
+ url_filter=args.filter,
215
+ console_level=args.console_level,
216
+ user_agent=args.user_agent,
217
+ cookies=cookies,
218
+ whitelist_path=args.whitelist,
219
+ accept_consent=(not args.no_consent) if args.no_consent is not None else None,
220
+ trigger_lazy_load=(not args.no_scroll) if args.no_scroll is not None else None,
221
+ respect_robots=(not args.ignore_robots) if args.ignore_robots is not None else None,
222
+ )
223
+ app.run()
224
+ finally:
225
+ reset_terminal_title()
226
+ _reset_mouse_tracking()
227
+
228
+
229
+ def _preinit_graphics_backend() -> None:
230
+ """Eager-Import textual-image vor App-Start.
231
+
232
+ textual-image sendet beim Import Escape-Queries ans Terminal (DA1
233
+ Primary Device Attributes, Cell-Size-Query). Diese MUESSEN vor
234
+ `App.run()` laufen, damit die Antworten von der Library konsumiert
235
+ werden und nicht in Textual-Inputs landen (typischer Effekt: kryptische
236
+ `[?61;...c<35;...M`-Strings im Filter-Eingabefeld).
237
+
238
+ Wir laufen NUR, wenn das Terminal ueberhaupt ein Grafik-Protokoll
239
+ unterstuetzen koennte (TGP/Sixel) - sonst spart das den Import auf
240
+ "dummen" Terminals (SSH ohne Mux, dumb-tty etc.).
241
+ """
242
+ if not _terminal_supports_graphics():
243
+ return
244
+ try:
245
+ import time
246
+
247
+ import textual_image.renderable # noqa: F401
248
+ import textual_image.widget # noqa: F401
249
+ from textual_image._terminal import get_cell_size
250
+
251
+ get_cell_size()
252
+ # Kurze Pause, damit das Terminal Zeit hat die Probe-Queries zu
253
+ # beantworten BEVOR Textual stdin uebernimmt - sonst hat man die
254
+ # Antworten zwar abgeschickt, sie kommen aber erst zurueck wenn
255
+ # der Cooked-Mode schon zu ist (Windows Terminal/mintty sind hier
256
+ # spuerbar langsamer als ein lokales xterm).
257
+ time.sleep(0.15)
258
+ except Exception:
259
+ pass
260
+
261
+
262
+ def _terminal_supports_graphics() -> bool:
263
+ """Schnelle Heuristik: kann das Terminal evtl. TGP oder Sixel?"""
264
+ term = os.environ.get("TERM", "").lower()
265
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
266
+ if os.environ.get("KITTY_WINDOW_ID"):
267
+ return True
268
+ if "kitty" in term or "ghostty" in term:
269
+ return True
270
+ if term_program in ("wezterm", "ghostty", "mintty", "iterm.app"):
271
+ return True
272
+ if os.environ.get("KONSOLE_VERSION"):
273
+ return True
274
+ if os.environ.get("WT_SESSION"):
275
+ return True
276
+ return term in ("foot", "xterm", "mlterm", "mintty") or "foot" in term
277
+
278
+
279
+ def _usage_examples() -> str:
280
+ """Gibt die Nutzungsbeispiele zurueck."""
281
+ return """
282
+ Examples:
283
+ console-error-scanner https://example.com
284
+ console-error-scanner https://example.com/sitemap.xml
285
+ console-error-scanner sitemap.xml
286
+ console-error-scanner https://example.com --concurrency 12
287
+ console-error-scanner https://example.com --lang en
288
+
289
+ Keybindings (TUI):
290
+ c = Crawl m = Load sitemap r = Report h = History s = Settings
291
+ w = Whitelist e = Errors only t = Theme l = Log
292
+ d = Copy detail F10 = Top 10 / = Filter i = Info q = Quit
293
+ """
294
+
295
+
296
+ def _enable_faulthandler() -> None:
297
+ """Faengt HARTE Abstuerze ab, die an Pythons Exception-Handling UND am
298
+ finally-Block vorbeilaufen: native Access Violation, Stack-Overflow, fataler
299
+ Interpreter-Fehler.
300
+
301
+ faulthandler schreibt den Traceback aller Threads in fault.log - separat vom
302
+ Terminal, das der Maus-Tracking-Muell sonst unlesbar macht. CrashGuard und
303
+ _persist_crash greifen nur bei normalen Python-Exceptions, dieser Handler bei
304
+ allem darunter. Die Startzeile ist die zweite Haelfte der Diagnose: steht
305
+ danach nichts weiter in der Datei, wurde der Prozess von aussen abgeraeumt.
306
+ """
307
+ import atexit
308
+ import contextlib
309
+ import faulthandler
310
+ from datetime import datetime
311
+
312
+ from console_error_scanner.models.settings import SETTINGS_FILE
313
+
314
+ global _fault_log
315
+ with contextlib.suppress(Exception):
316
+ verzeichnis = SETTINGS_FILE.parent
317
+ verzeichnis.mkdir(parents=True, exist_ok=True)
318
+ # Bewusst offen lassen (Prozess-Lebensdauer) - faulthandler schreibt
319
+ # beim fatalen Signal direkt in dieses Handle.
320
+ _fault_log = open(verzeichnis / "fault.log", "a", encoding="utf-8") # noqa: SIM115
321
+ stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
322
+ _fault_log.write(f"\n===== Start {stamp} - v{__version__} =====\n")
323
+ _fault_log.flush()
324
+ faulthandler.enable(file=_fault_log, all_threads=True)
325
+ # Gegenstueck zur Startzeile - siehe _write_fault_end.
326
+ atexit.register(_write_fault_end)
327
+
328
+
329
+ def _reset_mouse_tracking() -> None:
330
+ """Schaltet alle Maus-Tracking-Modi des Terminals ab (idempotent).
331
+
332
+ Schreibt bewusst nach sys.__stdout__, NICHT sys.stdout: Textual kapert
333
+ sys.stdout zur Laufzeit, ein Reset dorthin landet im Nichts und das Terminal
334
+ bleibt im Maus-Tracking-Modus haengen. Der garantierte Reset passiert im
335
+ Shell-Wrapper run.ps1 (laeuft auch nach einem harten Crash) - das hier ist
336
+ der zusaetzliche In-Prozess-Pfad.
337
+ """
338
+ stream = sys.__stdout__
339
+ if stream is None or not stream.isatty():
340
+ return
341
+ stream.write("[?1000l[?1002l[?1003l[?1006l[?1015l")
342
+ stream.flush()
343
+
344
+
345
+ def _write_fault_end() -> None:
346
+ """Schreibt die Endzeile der Sitzungsklammer (ueber atexit registriert).
347
+
348
+ Erst dieses Gegenstueck zur Startzeile macht die Datei aussagekraeftig:
349
+
350
+ Start + Ende -> sauber beendet
351
+ Start + Traceback -> Python-Fehler (der Handler hat ihn gesehen)
352
+ Start und sonst nichts -> Prozess hart abgeraeumt
353
+
354
+ Unter Windows hilft ein Signalhandler dabei nicht: ein Abbruch von aussen
355
+ laeuft dort ueber TerminateProcess und liefert dem Ziel kein abfangbares
356
+ Signal. Die FEHLENDE Endzeile ist der einzige Beleg.
357
+ """
358
+ import contextlib
359
+ from datetime import datetime
360
+
361
+ if _fault_log is None:
362
+ return
363
+ with contextlib.suppress(Exception):
364
+ stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
365
+ _fault_log.write(f"===== Ende {stamp} =====\n")
366
+ _fault_log.flush()
367
+
368
+
369
+ if __name__ == "__main__":
370
+ main()