From dbacfca5cbb4b37a38518be1f2ae252519b0d489 Mon Sep 17 00:00:00 2001 From: nagisml Date: Fri, 16 Oct 2026 21:05:00 +0200 Subject: [PATCH 1/8] Catch crashes and offer to report them Uncaught exceptions went to stderr, which a windowed build doesn't have, so they were lost, and a hard crash left no trace the user would find. - sys.excepthook / threading.excepthook log the traceback and show a "OpenSAK ran into a problem" dialog (Continue / Quit). Worker-thread exceptions are handed to the GUI thread through a queued signal. The same error shows the dialog only once per session. - faulthandler writes native crashes (segfaults in Qt/WebEngine) to crash_native.log. - A QLockFile session.lock is removed only on a clean exit. At the next start a lock left by a dead process means the last session crashed, and the user is offered a report with the native dump and the tail of opensak.log.previous. A second running instance is not taken for a crash. - The dialog shows the exact report text and can open a prefilled GitHub issue or a mail to hello@opensak.com, copy the report, or open the log folder so the log can be attached. Nothing is sent automatically; home paths, the user name, tokens and e-mail addresses are removed first. - New strings in all languages; tests in test_crash_report.py. --- src/opensak/app.py | 9 +- src/opensak/crash_report.py | 398 ++++++++++++++++++ .../gui/dialogs/crash_report_dialog.py | 120 ++++++ src/opensak/lang/cs.py | 12 + src/opensak/lang/da.py | 12 + src/opensak/lang/de.py | 12 + src/opensak/lang/de_CH.py | 12 + src/opensak/lang/en.py | 12 + src/opensak/lang/es.py | 12 + src/opensak/lang/fr.py | 12 + src/opensak/lang/nl.py | 12 + src/opensak/lang/pl.py | 12 + src/opensak/lang/pt.py | 12 + src/opensak/lang/se.py | 12 + tests/unit-tests/test_crash_report.py | 291 +++++++++++++ 15 files changed, 949 insertions(+), 1 deletion(-) create mode 100644 src/opensak/crash_report.py create mode 100644 src/opensak/gui/dialogs/crash_report_dialog.py create mode 100644 tests/unit-tests/test_crash_report.py diff --git a/src/opensak/app.py b/src/opensak/app.py index ebe61f0d..d2855c81 100644 --- a/src/opensak/app.py +++ b/src/opensak/app.py @@ -280,8 +280,14 @@ def main() -> None: # Initialiser logging-systemet FØRST (issue #232) — så vi kan logge # alt der sker under resten af opstarten, inkl. migration og wizard. from opensak.logger import setup_logging - setup_logging() + log_path = setup_logging() logger.info("startup: main() begin, version=%s", _ver) + + # Catch crashes from here on: uncaught exceptions get a report dialog + # instead of vanishing (no console in a windowed build), and a hard + # crash of the last session is detected so it can be reported below. + from opensak import crash_report + crash_report.install(log_path.parent) _startup_t0 = time.monotonic() # Kør macOS-sti-migreringen (issue #825) FØR NOGET ANDET rører @@ -468,6 +474,7 @@ def _close_splash(): QTimer.singleShot( 600, lambda: window.show_settings_restore_notice(settings_restore_result) ) + QTimer.singleShot(650, lambda: crash_report.show_previous_crash_notice(window)) window.show() exit_code = app.exec() diff --git a/src/opensak/crash_report.py b/src/opensak/crash_report.py new file mode 100644 index 00000000..d8a80cdf --- /dev/null +++ b/src/opensak/crash_report.py @@ -0,0 +1,398 @@ +""" +crash_report.py — Catch crashes and help the user report them. + +Two kinds of crash are covered: + + - Python exceptions nobody caught (in the GUI thread, a QThread or a + plain thread). sys.excepthook / threading.excepthook log the traceback + and show the crash dialog, so a windowed build no longer swallows them + silently on a non-existent stderr. + + - Hard crashes Python cannot react to (a segfault in Qt/WebEngine, the + process being killed). faulthandler writes the native stack to + crash_native.log, and a session lock file that is only removed on a + clean exit tells the next start that the last session did not end + normally — the user is then offered to report it with the previous + session's log (opensak.log.previous, issue #737). + +Nothing is ever sent automatically: the report goes to a prefilled GitHub +issue or e-mail only when the user clicks the button, and the text is +scrubbed of home paths, the user name and tokens first. +""" + +from __future__ import annotations + +import atexit +import faulthandler +import getpass +import logging +import os +import platform +import re +import sys +import threading +import traceback +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING +from urllib.parse import quote, urlencode + +if TYPE_CHECKING: + from PySide6.QtCore import QLockFile + +logger = logging.getLogger("opensak.crash") + +GITHUB_NEW_ISSUE_URL = "https://github.com/OpenSAK-Org/OpenSAK/issues/new" +SUPPORT_EMAIL = "hello@opensak.com" + +# GitHub refuses very long URLs (~8 KB); mail clients choke much earlier +# (Outlook/Windows mailto handlers at ~2 KB). The body is cut to fit. +_GITHUB_BODY_MAX = 6000 +_MAILTO_BODY_MAX = 1800 +# Lines of the previous session's log put in an unclean-shutdown report. +_LOG_TAIL_LINES = 40 + +_NATIVE_LOG_NAME = "crash_native.log" +_SESSION_LOCK_NAME = "session.lock" + + +@dataclass +class PreviousCrash: + """What is known about a previous session that did not exit cleanly.""" + native_dump: str + log_tail: str + + +# Module state — set by install(), cleared by shutdown(). +_installed = False +_native_file = None +_session_lock: "QLockFile | None" = None +_previous_crash: PreviousCrash | None = None +_bridge = None +_shown: set[str] = set() +_dialog_open = False + + +def _app_data_dir() -> Path: + from opensak.config import get_app_data_dir + return get_app_data_dir() + + +# ── Report text ────────────────────────────────────────────────────────────── + +def system_info() -> str: + """Version and platform lines put at the top of every report.""" + from opensak import __version__ + try: + from PySide6 import __version__ as pyside_version + except ImportError: # pragma: no cover - PySide6 is a hard dependency + pyside_version = "?" + try: + from opensak.msix import is_msix_packaged + msix = is_msix_packaged() + except Exception: + msix = False + if msix: + build = "Microsoft Store" + elif getattr(sys, "frozen", False): + build = "installer/frozen" + else: + build = "source" + return ( + f"OpenSAK: {__version__} ({build})\n" + f"OS: {platform.platform()}\n" + f"Python: {platform.python_version()}\n" + f"PySide6: {pyside_version}" + ) + + +_TOKEN_RE = re.compile( + r"(?i)((?:access|refresh|id)_?token|authorization|bearer|password|secret|api_?key)" + r"([\"']?\s*[:=]\s*[\"']?|\s+)((?:bearer\s+)?[^\s\"',}]+)" +) +_EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+") + + +def scrub(text: str) -> str: + """Remove personal data the user would not want in a public issue.""" + home = str(Path.home()) + if home and home not in ("/", "\\"): + # Both slash styles — tracebacks and logs mix them on Windows. + for variant in {home, home.replace("\\", "/"), home.replace("/", "\\")}: + text = re.sub(re.escape(variant), "~", text, flags=re.IGNORECASE) + try: + user = getpass.getuser() + except Exception: + user = "" + if len(user) >= 3: + text = re.sub(rf"(?i)(?", text) + text = _TOKEN_RE.sub(lambda m: f"{m.group(1)}{m.group(2)}", text) + text = _EMAIL_RE.sub( + lambda m: m.group(0) if m.group(0).lower() == SUPPORT_EMAIL else "", text + ) + return text + + +def format_exception_report(exc_type, exc_value, exc_tb) -> str: + """The full report text for an uncaught exception.""" + tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) + return scrub(f"{system_info()}\n\n{tb}").rstrip() + + +def format_previous_crash_report(crash: PreviousCrash) -> str: + """The full report text for a session that did not exit cleanly.""" + parts = [system_info(), "", "The previous OpenSAK session did not exit normally."] + if crash.native_dump.strip(): + parts += ["", "Native crash dump:", crash.native_dump.strip()] + if crash.log_tail.strip(): + parts += ["", f"Last {_LOG_TAIL_LINES} lines of the previous log:", crash.log_tail.strip()] + return scrub("\n".join(parts)).rstrip() + + +def report_title(report: str) -> str: + """A short issue title — the exception line of a traceback if there is one.""" + lines = [ln for ln in report.splitlines() if ln.strip()] + for line in reversed(lines): + if not line.startswith((" ", "\t")) and re.match(r"^[\w.]+(Error|Exception|Interrupt|Exit|Warning)\b", line): + return f"Crash: {line.strip()}"[:120] + return "Crash: OpenSAK did not exit normally" + + +def _fit(body: str, limit: int) -> str: + if len(body) <= limit: + return body + # Keep the head (version/OS) and the tail (where the exception is). + note = "\n\n[… shortened — please attach the log file …]\n\n" + keep = limit - len(note) + head = keep // 3 + return body[:head] + note + body[-(keep - head):] + + +def _log_hint(previous_session: bool) -> str: + if previous_session: + return "opensak.log.previous (Help → Open log from last restart)" + return "opensak.log (Help → Open log file)" + + +def _issue_body(report: str, previous_session: bool) -> str: + return ( + "**What were you doing when it happened?**\n\n\n\n" + "**Crash report**\n\n```\n" + report + "\n```\n\n" + f"_Please also drag {_log_hint(previous_session)} into this issue._\n" + ) + + +def github_issue_url(report: str, previous_session: bool = False) -> str: + """A prefilled 'new issue' URL. GitHub cannot attach files via URL.""" + # Shorten the report, not the wrapper, so the code fence stays closed. + overhead = len(_issue_body("", previous_session)) + body = _issue_body(_fit(report, _GITHUB_BODY_MAX - overhead), previous_session) + return GITHUB_NEW_ISSUE_URL + "?" + urlencode( + {"title": report_title(report), "body": body, "labels": "bug"}, + quote_via=quote, + ) + + +def mailto_url(report: str, previous_session: bool = False) -> str: + """A mailto: link with the (shortened) report as the body.""" + body = ( + "What were you doing when it happened?\n\n\n\n" + + _fit(report, _MAILTO_BODY_MAX) + + f"\n\nPlease attach {_log_hint(previous_session)}.\n" + ) + return ( + f"mailto:{SUPPORT_EMAIL}?subject={quote(report_title(report))}" + f"&body={quote(body)}" + ) + + +# ── Unclean-shutdown detection ────────────────────────────────────────────── + +def _read_tail(path: Path, lines: int) -> str: + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + return "" + return "\n".join(text.splitlines()[-lines:]) + + +def _detect_previous_crash(app_dir: Path) -> PreviousCrash | None: + """ + Take this session's lock and report whether the last session crashed. + + QLockFile stores the owning PID: if the file is still there but tryLock() + succeeds, its owner is gone without unlocking — it crashed or was killed. + If tryLock() fails another OpenSAK is running right now, which is not a + crash (and this instance then runs without a lock of its own). + """ + global _session_lock + from PySide6.QtCore import QLockFile + + lock_path = app_dir / _SESSION_LOCK_NAME + existed = lock_path.exists() + lock = QLockFile(str(lock_path)) + lock.setStaleLockTime(0) # only a dead PID makes it stale, never age + if not lock.tryLock(0): + logger.info("crash: another OpenSAK instance holds the session lock") + return None + _session_lock = lock + if not existed: + return None + + from opensak.config import get_previous_log_path + native = _read_tail(app_dir / _NATIVE_LOG_NAME, 200) + logger.warning("crash: previous session did not exit cleanly") + return PreviousCrash( + native_dump=native, + log_tail=_read_tail(get_previous_log_path(), _LOG_TAIL_LINES), + ) + + +def _enable_faulthandler(app_dir: Path) -> None: + global _native_file + try: + # "w" — the dump of the previous session has been read by now. + _native_file = open(app_dir / _NATIVE_LOG_NAME, "w", encoding="utf-8") + faulthandler.enable(file=_native_file, all_threads=True) + except (OSError, RuntimeError): + logger.warning("crash: could not enable faulthandler", exc_info=True) + + +# ── Hooks ──────────────────────────────────────────────────────────────────── + +def _excepthook(exc_type, exc_value, exc_tb) -> None: + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_tb) + return + _handle(exc_type, exc_value, exc_tb) + + +def _thread_excepthook(args: threading.ExceptHookArgs) -> None: + if args.exc_type is SystemExit: + return + _handle(args.exc_type, args.exc_value, args.exc_traceback) + + +def _handle(exc_type, exc_value, exc_tb) -> None: + """Log the exception and hand it to the GUI thread for the dialog.""" + try: + logger.critical( + "Uncaught exception", exc_info=(exc_type, exc_value, exc_tb) + ) + report = format_exception_report(exc_type, exc_value, exc_tb) + except Exception: + # Never let the reporter itself raise out of an excepthook. + sys.__excepthook__(exc_type, exc_value, exc_tb) + return + if _bridge is not None: + # A queued signal when called from a worker thread, direct otherwise. + _bridge.crashed.emit(report) + + +def _show_exception_dialog(report: str) -> None: + global _dialog_open + # The same exception repeating (e.g. in a paint event) would otherwise + # open one dialog after another; it is still logged every time. + key = report.splitlines()[-1] if report else "" + if _dialog_open or key in _shown: + return + _shown.add(key) + _dialog_open = True + try: + from opensak.gui.dialogs.crash_report_dialog import CrashReportDialog + dlg = CrashReportDialog(report, previous_session=False) + dlg.exec() + if dlg.quit_requested: + # Not app.quit(): the error may have left the window in a state + # where a normal close (and its exit backup) fails again. The + # database is in WAL mode, so a hard exit loses nothing committed. + shutdown() + logging.shutdown() + os._exit(1) + except Exception: + logger.exception("crash: could not show the crash dialog") + finally: + _dialog_open = False + + +def install(app_dir: Path | None = None) -> None: + """ + Install the crash hooks. Call once from app.main(), after logging is set + up and the QApplication exists. Must run before anything shows a window. + """ + global _installed, _previous_crash, _bridge + if _installed: + return + app_dir = app_dir or _app_data_dir() + try: + _previous_crash = _detect_previous_crash(app_dir) + except Exception: + logger.warning("crash: unclean-shutdown check failed", exc_info=True) + _enable_faulthandler(app_dir) + + from PySide6.QtCore import QObject, Signal, Slot + + class _Bridge(QObject): + # The slot is a method of this GUI-thread object (not a plain + # function), so an emit from a worker thread is queued to the GUI + # thread instead of running the dialog in the worker. + crashed = Signal(str) + + def __init__(self) -> None: + super().__init__() + self.crashed.connect(self._on_crashed) + + @Slot(str) + def _on_crashed(self, report: str) -> None: + _show_exception_dialog(report) + + _bridge = _Bridge() + + sys.excepthook = _excepthook + threading.excepthook = _thread_excepthook + # Any normal interpreter exit (incl. sys.exit() after a startup error + # the user was already told about) counts as clean; a native crash or + # a kill never gets here, so its lock file stays behind. + atexit.register(shutdown) + _installed = True + + +def take_previous_crash() -> PreviousCrash | None: + """The previous session's crash, once — None if there was none.""" + global _previous_crash + crash, _previous_crash = _previous_crash, None + return crash + + +def show_previous_crash_notice(parent=None) -> None: + """If the last session crashed, offer to report it. Call after startup.""" + crash = take_previous_crash() + if crash is None: + return + try: + from opensak.gui.dialogs.crash_report_dialog import CrashReportDialog + CrashReportDialog( + format_previous_crash_report(crash), previous_session=True, parent=parent + ).exec() + except Exception: + logger.exception("crash: could not show the previous-crash dialog") + + +def shutdown() -> None: + """Mark the session as cleanly ended. Call when the app exits normally.""" + global _native_file, _session_lock, _installed, _bridge + if _session_lock is not None: + _session_lock.unlock() + _session_lock = None + if _native_file is not None: + try: + faulthandler.disable() + _native_file.close() + except (OSError, ValueError): + pass + _native_file = None + if _installed: + sys.excepthook = sys.__excepthook__ + threading.excepthook = threading.__excepthook__ + _bridge = None + _installed = False diff --git a/src/opensak/gui/dialogs/crash_report_dialog.py b/src/opensak/gui/dialogs/crash_report_dialog.py new file mode 100644 index 00000000..9709a4d0 --- /dev/null +++ b/src/opensak/gui/dialogs/crash_report_dialog.py @@ -0,0 +1,120 @@ +""" +src/opensak/gui/dialogs/crash_report_dialog.py — "OpenSAK ran into a +problem" dialog (see opensak/crash_report.py). + +Shown for an uncaught exception, and at startup when the previous session +did not exit cleanly. The scrubbed report is shown read-only so the user +sees exactly what would be shared; nothing leaves the machine until they +click GitHub or e-mail, and both only open a prefilled page/mail. +""" + +from __future__ import annotations + +from PySide6.QtCore import QUrl +from PySide6.QtGui import QDesktopServices, QFontDatabase +from PySide6.QtWidgets import ( + QApplication, + QDialog, + QHBoxLayout, + QLabel, + QPlainTextEdit, + QPushButton, + QVBoxLayout, + QWidget, +) + +from opensak import crash_report +from opensak.gui.dialogs.widgets import clamp_dialog_height_to_screen +from opensak.lang import tr + + +class CrashReportDialog(QDialog): + def __init__( + self, + report: str, + previous_session: bool, + parent: QWidget | None = None, + ) -> None: + super().__init__(parent) + self._report = report + self._previous_session = previous_session + self.quit_requested = False + self.setWindowTitle(tr("crash_dialog_title")) + self.setMinimumWidth(620) + self._setup_ui() + clamp_dialog_height_to_screen(self, parent) + + def _setup_ui(self) -> None: + layout = QVBoxLayout(self) + + intro_key = "crash_dialog_previous_msg" if self._previous_session else "crash_dialog_msg" + intro = QLabel(tr(intro_key, email=crash_report.SUPPORT_EMAIL)) + intro.setWordWrap(True) + layout.addWidget(intro) + + self._text = QPlainTextEdit(self._report) + self._text.setReadOnly(True) + self._text.setFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)) + self._text.setMinimumHeight(220) + layout.addWidget(self._text, 1) + + privacy = QLabel(tr("crash_dialog_privacy")) + privacy.setWordWrap(True) + privacy.setStyleSheet("color: gray;") + layout.addWidget(privacy) + + report_row = QHBoxLayout() + self._btn_github = QPushButton(tr("crash_dialog_github")) + self._btn_github.clicked.connect(self._open_github) + self._btn_email = QPushButton(tr("crash_dialog_email")) + self._btn_email.clicked.connect(self._open_email) + self._btn_copy = QPushButton(tr("crash_dialog_copy")) + self._btn_copy.clicked.connect(self._copy) + self._btn_log = QPushButton(tr("crash_dialog_show_log")) + self._btn_log.clicked.connect(self._show_log) + for btn in (self._btn_github, self._btn_email, self._btn_copy, self._btn_log): + report_row.addWidget(btn) + layout.addLayout(report_row) + + close_row = QHBoxLayout() + close_row.addStretch() + if not self._previous_session: + self._btn_quit = QPushButton(tr("crash_dialog_quit")) + self._btn_quit.clicked.connect(self._quit) + close_row.addWidget(self._btn_quit) + self._btn_close = QPushButton( + tr("close" if self._previous_session else "crash_dialog_continue") + ) + self._btn_close.setDefault(True) + self._btn_close.clicked.connect(self.accept) + close_row.addWidget(self._btn_close) + layout.addLayout(close_row) + + # ── Actions ────────────────────────────────────────────────────────────── + + def _open_github(self) -> None: + QDesktopServices.openUrl( + QUrl(crash_report.github_issue_url(self._report, self._previous_session)) + ) + # The issue page cannot take the log via URL — show it to drag in. + self._show_log() + + def _open_email(self) -> None: + QDesktopServices.openUrl( + QUrl(crash_report.mailto_url(self._report, self._previous_session)) + ) + self._show_log() + + def _copy(self) -> None: + QApplication.clipboard().setText(self._report) + self._btn_copy.setText(tr("crash_dialog_copied")) + + def _show_log(self) -> None: + """Open the folder holding the log, so it can be dragged/attached.""" + from opensak.config import get_log_path, get_previous_log_path + log = get_previous_log_path() if self._previous_session else get_log_path() + QDesktopServices.openUrl(QUrl.fromLocalFile(str(log.parent))) + + def _quit(self) -> None: + self.quit_requested = True + self.reject() diff --git a/src/opensak/lang/cs.py b/src/opensak/lang/cs.py index d90c0733..951960ae 100644 --- a/src/opensak/lang/cs.py +++ b/src/opensak/lang/cs.py @@ -1802,4 +1802,16 @@ "gsak_filter_import_where_hint": " Otevřete takový filtr v Nastavit filtr → Where a dokončete je tam.", "gsak_filter_import_skipped_header": "{count} filtrů přeskočeno — profil s tímto názvem už existuje:", "gsak_filter_import_failed_header": "{count} filtrů se nepodařilo převést:", + + "crash_dialog_title": "OpenSAK narazil na problém", + "crash_dialog_msg": "V OpenSAK došlo k neočekávané chybě. Můžete zkusit pokračovat, ale bezpečnější je uložit práci a restartovat.\n\nPomozte nám ji opravit: nahlaste ji na GitHubu nebo ji pošlete na {email} a přiložte soubor protokolu.", + "crash_dialog_previous_msg": "OpenSAK se při posledním spuštění neukončil normálně — mohl spadnout.\n\nPokud si pamatujete, co jste dělali, nahlaste to prosím na GitHubu nebo to pošlete na {email} a přiložte soubor protokolu.", + "crash_dialog_privacy": "Přesně toto bude sdíleno. Nic se neodesílá automaticky — tlačítka pouze otevřou předvyplněné hlášení na GitHubu nebo e-mail. Vaše domovská složka, uživatelské jméno a tokeny byly odstraněny.", + "crash_dialog_github": "Nahlásit na GitHubu", + "crash_dialog_email": "Odeslat e-mailem", + "crash_dialog_copy": "Kopírovat hlášení", + "crash_dialog_copied": "✓ Zkopírováno", + "crash_dialog_show_log": "Zobrazit soubor protokolu", + "crash_dialog_quit": "Ukončit OpenSAK", + "crash_dialog_continue": "Pokračovat", } diff --git a/src/opensak/lang/da.py b/src/opensak/lang/da.py index 1231aabc..3e838ff4 100644 --- a/src/opensak/lang/da.py +++ b/src/opensak/lang/da.py @@ -1806,4 +1806,16 @@ "gsak_filter_import_where_hint": " Åbn et sådant filter under Sæt filter → Where for at se og færdiggøre dem.", "gsak_filter_import_skipped_header": "{count} filtre sprunget over — en profil med det navn findes allerede:", "gsak_filter_import_failed_header": "{count} filtre kunne ikke konverteres:", + + "crash_dialog_title": "OpenSAK stødte på et problem", + "crash_dialog_msg": "OpenSAK ramte en uventet fejl. Du kan prøve at fortsætte, men det er sikrest at gemme dit arbejde og genstarte.\n\nHjælp os med at rette den: rapportér den på GitHub eller send den til {email}, og vedhæft logfilen.", + "crash_dialog_previous_msg": "OpenSAK blev ikke lukket normalt sidst den kørte — den kan være gået ned.\n\nHvis du husker, hvad du lavede, så rapportér det venligst på GitHub eller send det til {email}, og vedhæft logfilen.", + "crash_dialog_privacy": "Præcis dette bliver delt. Intet sendes automatisk — knapperne åbner kun en udfyldt GitHub-sag eller e-mail. Din hjemmemappe, dit brugernavn og tokens er fjernet.", + "crash_dialog_github": "Rapportér på GitHub", + "crash_dialog_email": "Send via e-mail", + "crash_dialog_copy": "Kopiér rapport", + "crash_dialog_copied": "✓ Kopieret", + "crash_dialog_show_log": "Vis logfil", + "crash_dialog_quit": "Afslut OpenSAK", + "crash_dialog_continue": "Fortsæt", } diff --git a/src/opensak/lang/de.py b/src/opensak/lang/de.py index 124db02a..508619b3 100644 --- a/src/opensak/lang/de.py +++ b/src/opensak/lang/de.py @@ -1807,4 +1807,16 @@ "gsak_filter_import_where_hint": " Solche Filter unter Filter setzen → Where öffnen und dort fertigstellen.", "gsak_filter_import_skipped_header": "{count} Filter übersprungen — ein Profil dieses Namens existiert bereits:", "gsak_filter_import_failed_header": "{count} Filter konnten nicht umgewandelt werden:", + + "crash_dialog_title": "OpenSAK ist auf ein Problem gestoßen", + "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", + "crash_dialog_github": "Auf GitHub melden", + "crash_dialog_email": "Per E-Mail senden", + "crash_dialog_copy": "Bericht kopieren", + "crash_dialog_copied": "✓ Kopiert", + "crash_dialog_show_log": "Protokolldatei anzeigen", + "crash_dialog_quit": "OpenSAK beenden", + "crash_dialog_continue": "Fortfahren", } diff --git a/src/opensak/lang/de_CH.py b/src/opensak/lang/de_CH.py index e8c70771..ff36770d 100644 --- a/src/opensak/lang/de_CH.py +++ b/src/opensak/lang/de_CH.py @@ -1808,4 +1808,16 @@ "gsak_filter_import_where_hint": " Solche Filter unter Filter setzen → Where öffnen und dort fertigstellen.", "gsak_filter_import_skipped_header": "{count} Filter übersprungen — ein Profil dieses Namens existiert bereits:", "gsak_filter_import_failed_header": "{count} Filter konnten nicht umgewandelt werden:", + + "crash_dialog_title": "OpenSAK ist auf ein Problem gestossen", + "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", + "crash_dialog_github": "Auf GitHub melden", + "crash_dialog_email": "Per E-Mail senden", + "crash_dialog_copy": "Bericht kopieren", + "crash_dialog_copied": "✓ Kopiert", + "crash_dialog_show_log": "Protokolldatei anzeigen", + "crash_dialog_quit": "OpenSAK beenden", + "crash_dialog_continue": "Fortfahren", } diff --git a/src/opensak/lang/en.py b/src/opensak/lang/en.py index 0d4d59e3..59aa7598 100644 --- a/src/opensak/lang/en.py +++ b/src/opensak/lang/en.py @@ -1805,4 +1805,16 @@ "gsak_filter_import_where_hint": " Open such a filter under Set filter → Where to see and finish them.", "gsak_filter_import_skipped_header": "{count} filter(s) skipped — a profile of that name already exists:", "gsak_filter_import_failed_header": "{count} filter(s) could not be converted:", + + "crash_dialog_title": "OpenSAK ran into a problem", + "crash_dialog_msg": "OpenSAK hit an unexpected error. You can try to continue, but it is safer to save your work and restart.\n\nPlease help us fix it: report it on GitHub or send it to {email}, and attach the log file.", + "crash_dialog_previous_msg": "OpenSAK did not close normally the last time it ran — it may have crashed.\n\nIf you remember what you were doing, please report it on GitHub or send it to {email}, and attach the log file.", + "crash_dialog_privacy": "This is exactly what will be shared. Nothing is sent automatically — the buttons only open a prefilled GitHub issue or e-mail. Your home folder, user name and tokens have been removed.", + "crash_dialog_github": "Report on GitHub", + "crash_dialog_email": "Send by e-mail", + "crash_dialog_copy": "Copy report", + "crash_dialog_copied": "✓ Copied", + "crash_dialog_show_log": "Show log file", + "crash_dialog_quit": "Quit OpenSAK", + "crash_dialog_continue": "Continue", } diff --git a/src/opensak/lang/es.py b/src/opensak/lang/es.py index d6458138..4e55a53c 100644 --- a/src/opensak/lang/es.py +++ b/src/opensak/lang/es.py @@ -1807,4 +1807,16 @@ "gsak_filter_import_where_hint": " Abre ese filtro en Definir filtro → Where para verlas y terminarlas.", "gsak_filter_import_skipped_header": "{count} filtro(s) omitido(s): ya existe un perfil con ese nombre:", "gsak_filter_import_failed_header": "No se pudieron convertir {count} filtro(s):", + + "crash_dialog_title": "OpenSAK ha encontrado un problema", + "crash_dialog_msg": "OpenSAK ha sufrido un error inesperado. Puedes intentar continuar, pero es más seguro guardar tu trabajo y reiniciar.\n\nAyúdanos a corregirlo: infórmalo en GitHub o envíalo a {email}, y adjunta el archivo de registro.", + "crash_dialog_previous_msg": "OpenSAK no se cerró correctamente la última vez que se ejecutó; puede que se haya bloqueado.\n\nSi recuerdas lo que estabas haciendo, infórmalo en GitHub o envíalo a {email}, y adjunta el archivo de registro.", + "crash_dialog_privacy": "Esto es exactamente lo que se compartirá. No se envía nada automáticamente: los botones solo abren una incidencia de GitHub o un correo ya rellenados. Se han eliminado tu carpeta personal, tu nombre de usuario y los tokens.", + "crash_dialog_github": "Informar en GitHub", + "crash_dialog_email": "Enviar por correo", + "crash_dialog_copy": "Copiar informe", + "crash_dialog_copied": "✓ Copiado", + "crash_dialog_show_log": "Mostrar archivo de registro", + "crash_dialog_quit": "Salir de OpenSAK", + "crash_dialog_continue": "Continuar", } diff --git a/src/opensak/lang/fr.py b/src/opensak/lang/fr.py index 9beeef8d..04625bc3 100644 --- a/src/opensak/lang/fr.py +++ b/src/opensak/lang/fr.py @@ -1807,4 +1807,16 @@ "gsak_filter_import_where_hint": " Ouvrez un tel filtre via Définir le filtre → Where pour les voir et les finir.", "gsak_filter_import_skipped_header": "{count} filtre(s) ignoré(s) — un profil de ce nom existe déjà :", "gsak_filter_import_failed_header": "{count} filtre(s) n'ont pas pu être convertis :", + + "crash_dialog_title": "OpenSAK a rencontré un problème", + "crash_dialog_msg": "OpenSAK a rencontré une erreur inattendue. Vous pouvez essayer de continuer, mais il est plus sûr d'enregistrer votre travail et de redémarrer.\n\nAidez-nous à la corriger : signalez-la sur GitHub ou envoyez-la à {email}, en joignant le fichier journal.", + "crash_dialog_previous_msg": "OpenSAK ne s'est pas fermé normalement lors de sa dernière exécution — il a peut-être planté.\n\nSi vous vous souvenez de ce que vous faisiez, signalez-le sur GitHub ou envoyez-le à {email}, en joignant le fichier journal.", + "crash_dialog_privacy": "C'est exactement ce qui sera partagé. Rien n'est envoyé automatiquement — les boutons ouvrent seulement un ticket GitHub ou un e-mail pré-rempli. Votre dossier personnel, votre nom d'utilisateur et vos jetons ont été supprimés.", + "crash_dialog_github": "Signaler sur GitHub", + "crash_dialog_email": "Envoyer par e-mail", + "crash_dialog_copy": "Copier le rapport", + "crash_dialog_copied": "✓ Copié", + "crash_dialog_show_log": "Afficher le fichier journal", + "crash_dialog_quit": "Quitter OpenSAK", + "crash_dialog_continue": "Continuer", } diff --git a/src/opensak/lang/nl.py b/src/opensak/lang/nl.py index c630af99..83a1a6b9 100644 --- a/src/opensak/lang/nl.py +++ b/src/opensak/lang/nl.py @@ -1804,4 +1804,16 @@ "gsak_filter_import_where_hint": " Open zo'n filter via Filter instellen → Where om ze te zien en af te maken.", "gsak_filter_import_skipped_header": "{count} filter(s) overgeslagen — een profiel met die naam bestaat al:", "gsak_filter_import_failed_header": "{count} filter(s) konden niet omgezet worden:", + + "crash_dialog_title": "OpenSAK is tegen een probleem aangelopen", + "crash_dialog_msg": "Er is een onverwachte fout opgetreden in OpenSAK. U kunt proberen door te gaan, maar het is veiliger om uw werk op te slaan en opnieuw te starten.\n\nHelp ons de fout op te lossen: meld deze op GitHub of stuur deze naar {email}, en voeg het logbestand toe.", + "crash_dialog_previous_msg": "OpenSAK is de vorige keer niet normaal afgesloten — mogelijk is het gecrasht.\n\nAls u nog weet wat u aan het doen was, meld het dan op GitHub of stuur het naar {email}, en voeg het logbestand toe.", + "crash_dialog_privacy": "Precies dit wordt gedeeld. Er wordt niets automatisch verzonden — de knoppen openen alleen een vooraf ingevuld GitHub-issue of e-mail. Uw thuismap, gebruikersnaam en tokens zijn verwijderd.", + "crash_dialog_github": "Melden op GitHub", + "crash_dialog_email": "Versturen per e-mail", + "crash_dialog_copy": "Rapport kopiëren", + "crash_dialog_copied": "✓ Gekopieerd", + "crash_dialog_show_log": "Logbestand tonen", + "crash_dialog_quit": "OpenSAK afsluiten", + "crash_dialog_continue": "Doorgaan", } diff --git a/src/opensak/lang/pl.py b/src/opensak/lang/pl.py index 5137c9f0..85fd1d11 100644 --- a/src/opensak/lang/pl.py +++ b/src/opensak/lang/pl.py @@ -1807,4 +1807,16 @@ "gsak_filter_import_where_hint": " Otwórz taki filtr w Ustaw filtr → Where, aby je zobaczyć i dokończyć.", "gsak_filter_import_skipped_header": "{count} filtrów pominięto — profil o tej nazwie już istnieje:", "gsak_filter_import_failed_header": "Nie udało się przekonwertować {count} filtrów:", + + "crash_dialog_title": "OpenSAK napotkał problem", + "crash_dialog_msg": "W OpenSAK wystąpił nieoczekiwany błąd. Możesz spróbować kontynuować, ale bezpieczniej jest zapisać pracę i uruchomić program ponownie.\n\nPomóż nam go naprawić: zgłoś go na GitHubie lub wyślij na {email} i dołącz plik dziennika.", + "crash_dialog_previous_msg": "OpenSAK nie został poprawnie zamknięty podczas ostatniego uruchomienia — mógł ulec awarii.\n\nJeśli pamiętasz, co robiłeś, zgłoś to na GitHubie lub wyślij na {email} i dołącz plik dziennika.", + "crash_dialog_privacy": "Dokładnie to zostanie udostępnione. Nic nie jest wysyłane automatycznie — przyciski otwierają jedynie wstępnie wypełnione zgłoszenie na GitHubie lub e-mail. Twój folder domowy, nazwa użytkownika i tokeny zostały usunięte.", + "crash_dialog_github": "Zgłoś na GitHubie", + "crash_dialog_email": "Wyślij e-mailem", + "crash_dialog_copy": "Kopiuj raport", + "crash_dialog_copied": "✓ Skopiowano", + "crash_dialog_show_log": "Pokaż plik dziennika", + "crash_dialog_quit": "Zamknij OpenSAK", + "crash_dialog_continue": "Kontynuuj", } diff --git a/src/opensak/lang/pt.py b/src/opensak/lang/pt.py index ffc815bc..a16822e3 100644 --- a/src/opensak/lang/pt.py +++ b/src/opensak/lang/pt.py @@ -1807,4 +1807,16 @@ "gsak_filter_import_where_hint": " Abra esse filtro em Definir filtro → Where para as ver e terminar.", "gsak_filter_import_skipped_header": "{count} filtro(s) ignorado(s) — já existe um perfil com esse nome:", "gsak_filter_import_failed_header": "{count} filtro(s) não puderam ser convertidos:", + + "crash_dialog_title": "O OpenSAK encontrou um problema", + "crash_dialog_msg": "O OpenSAK encontrou um erro inesperado. Pode tentar continuar, mas é mais seguro guardar o seu trabalho e reiniciar.\n\nAjude-nos a corrigi-lo: reporte-o no GitHub ou envie-o para {email}, e anexe o ficheiro de registo.", + "crash_dialog_previous_msg": "O OpenSAK não foi fechado normalmente da última vez que foi executado — pode ter falhado.\n\nSe se lembrar do que estava a fazer, reporte-o no GitHub ou envie-o para {email}, e anexe o ficheiro de registo.", + "crash_dialog_privacy": "É exatamente isto que será partilhado. Nada é enviado automaticamente — os botões apenas abrem um issue do GitHub ou um e-mail pré-preenchido. A sua pasta pessoal, nome de utilizador e tokens foram removidos.", + "crash_dialog_github": "Reportar no GitHub", + "crash_dialog_email": "Enviar por e-mail", + "crash_dialog_copy": "Copiar relatório", + "crash_dialog_copied": "✓ Copiado", + "crash_dialog_show_log": "Mostrar ficheiro de registo", + "crash_dialog_quit": "Sair do OpenSAK", + "crash_dialog_continue": "Continuar", } diff --git a/src/opensak/lang/se.py b/src/opensak/lang/se.py index fc2b56a9..823f213a 100644 --- a/src/opensak/lang/se.py +++ b/src/opensak/lang/se.py @@ -1806,4 +1806,16 @@ "gsak_filter_import_where_hint": " Öppna ett sådant filter under Ange filter → Where för att se och slutföra dem.", "gsak_filter_import_skipped_header": "{count} filter överhoppade — en profil med det namnet finns redan:", "gsak_filter_import_failed_header": "{count} filter kunde inte konverteras:", + + "crash_dialog_title": "OpenSAK stötte på ett problem", + "crash_dialog_msg": "Ett oväntat fel uppstod i OpenSAK. Du kan försöka fortsätta, men det är säkrare att spara ditt arbete och starta om.\n\nHjälp oss att rätta det: rapportera det på GitHub eller skicka det till {email}, och bifoga loggfilen.", + "crash_dialog_previous_msg": "OpenSAK stängdes inte normalt senast det kördes — det kan ha kraschat.\n\nOm du minns vad du gjorde, rapportera det på GitHub eller skicka det till {email}, och bifoga loggfilen.", + "crash_dialog_privacy": "Exakt detta kommer att delas. Inget skickas automatiskt — knapparna öppnar bara ett förifyllt GitHub-ärende eller e-postmeddelande. Din hemmapp, ditt användarnamn och tokens har tagits bort.", + "crash_dialog_github": "Rapportera på GitHub", + "crash_dialog_email": "Skicka via e-post", + "crash_dialog_copy": "Kopiera rapport", + "crash_dialog_copied": "✓ Kopierat", + "crash_dialog_show_log": "Visa loggfil", + "crash_dialog_quit": "Avsluta OpenSAK", + "crash_dialog_continue": "Fortsätt", } diff --git a/tests/unit-tests/test_crash_report.py b/tests/unit-tests/test_crash_report.py new file mode 100644 index 00000000..3e608792 --- /dev/null +++ b/tests/unit-tests/test_crash_report.py @@ -0,0 +1,291 @@ +# tests/unit-tests/test_crash_report.py — crash catching and reporting. + +import faulthandler +import subprocess +import sys +import threading +from pathlib import Path +from urllib.parse import parse_qs, unquote, urlsplit + +import pytest + +from opensak import crash_report as cr + + +@pytest.fixture(autouse=True) +def _isolate(tmp_path, monkeypatch): + from opensak import settings_store as ss + monkeypatch.setattr(ss, "get_install_dir", lambda: tmp_path) + monkeypatch.setattr(cr, "_shown", set()) + monkeypatch.setattr(cr, "_previous_crash", None) + old_hook, old_thread_hook = sys.excepthook, threading.excepthook + faulthandler_was_on = faulthandler.is_enabled() + yield + cr.shutdown() + sys.excepthook, threading.excepthook = old_hook, old_thread_hook + # shutdown() disables faulthandler; give pytest's own back. + if faulthandler_was_on: + faulthandler.enable(file=sys.__stderr__) + + +def _raise_and_capture(exc: BaseException): + try: + raise exc + except BaseException: + return sys.exc_info() + + +# ── Report text ────────────────────────────────────────────────────────────── + +class TestScrub: + def test_removes_home_folder_both_slash_styles(self): + home = str(Path.home()) + text = f"File \"{home}\\a.py\" and {home.replace(chr(92), '/')}/b.py" + out = cr.scrub(text) + assert home not in out + assert home.replace("\\", "/") not in out + assert "~" in out + + def test_removes_user_name(self, monkeypatch): + monkeypatch.setattr(cr.getpass, "getuser", lambda: "geocacher42") + assert cr.scrub("D:\\data\\geocacher42\\db") == "D:\\data\\\\db" + + def test_short_user_name_is_left_alone(self, monkeypatch): + # "ab" would match far too much ordinary text. + monkeypatch.setattr(cr.getpass, "getuser", lambda: "ab") + assert cr.scrub("tab ab") == "tab ab" + + @pytest.mark.parametrize("text", [ + 'access_token="abc.def"', + "refresh_token: abc.def", + "Authorization: Bearer abc.def", + "{'password': 'abc.def'}", + ]) + def test_redacts_secrets(self, text): + out = cr.scrub(text) + assert "abc.def" not in out + assert "" in out + + def test_redacts_email_but_keeps_support_address(self): + out = cr.scrub("from me@example.org to hello@opensak.com") + assert "me@example.org" not in out + assert "hello@opensak.com" in out + + +class TestReports: + def test_exception_report_has_system_info_and_traceback(self): + report = cr.format_exception_report(*_raise_and_capture(ValueError("boom"))) + assert report.startswith("OpenSAK: ") + assert "Traceback" in report + assert report.endswith("ValueError: boom") + + def test_title_is_the_exception_line(self): + report = cr.format_exception_report(*_raise_and_capture(KeyError("gc_code"))) + assert cr.report_title(report) == "Crash: KeyError: 'gc_code'" + + def test_title_for_unclean_shutdown(self): + report = cr.format_previous_crash_report(cr.PreviousCrash("", "")) + assert cr.report_title(report) == "Crash: OpenSAK did not exit normally" + + def test_previous_report_includes_dump_and_log(self): + report = cr.format_previous_crash_report( + cr.PreviousCrash("Fatal Python error: Segmentation fault", "last log line") + ) + assert "Segmentation fault" in report + assert "last log line" in report + + +class TestUrls: + def test_github_url_is_prefilled_and_bounded(self): + report = cr.format_exception_report(*_raise_and_capture(RuntimeError("x" * 20000))) + url = cr.github_issue_url(report) + assert url.startswith(cr.GITHUB_NEW_ISSUE_URL + "?") + query = parse_qs(urlsplit(url).query) + body = query["body"][0] + assert len(body) <= cr._GITHUB_BODY_MAX + assert body.count("```") == 2 # the code fence survives shortening + assert "shortened" in body + assert query["title"][0].startswith("Crash: RuntimeError") + assert "opensak.log (Help" in body + + def test_github_url_previous_session_points_to_previous_log(self): + url = cr.github_issue_url("report", previous_session=True) + assert "opensak.log.previous" in parse_qs(urlsplit(url).query)["body"][0] + + def test_mailto_is_addressed_and_bounded(self): + report = cr.format_exception_report(*_raise_and_capture(RuntimeError("y" * 20000))) + url = cr.mailto_url(report) + assert url.startswith("mailto:hello@opensak.com?subject=Crash") + body = unquote(url.split("&body=", 1)[1]) + assert len(body) < cr._MAILTO_BODY_MAX + 200 + + +# ── Unclean-shutdown detection ────────────────────────────────────────────── + +def _leave_stale_lock(path: Path) -> None: + """Take the lock in another process that then dies without unlocking.""" + code = ( + "import os, sys\n" + "from PySide6.QtCore import QLockFile\n" + "l = QLockFile(sys.argv[1])\n" + "assert l.tryLock(0)\n" + "os._exit(0)\n" + ) + subprocess.run([sys.executable, "-c", code, str(path)], check=True) + assert path.exists() + + +class TestPreviousCrash: + def test_first_start_is_not_a_crash(self, tmp_path): + assert cr._detect_previous_crash(tmp_path) is None + assert (tmp_path / cr._SESSION_LOCK_NAME).exists() + + def test_clean_exit_is_not_a_crash(self, tmp_path): + cr.install(tmp_path) + cr.shutdown() + assert not (tmp_path / cr._SESSION_LOCK_NAME).exists() + cr.install(tmp_path) + assert cr.take_previous_crash() is None + + def test_lock_left_by_dead_process_is_a_crash(self, tmp_path): + _leave_stale_lock(tmp_path / cr._SESSION_LOCK_NAME) + (tmp_path / cr._NATIVE_LOG_NAME).write_text("Fatal Python error: boom", encoding="utf-8") + (tmp_path / "opensak.log.previous").write_text( + "\n".join(f"line {i}" for i in range(100)), encoding="utf-8" + ) + cr.install(tmp_path) + crash = cr.take_previous_crash() + assert crash is not None + assert "boom" in crash.native_dump + assert crash.log_tail.splitlines()[0] == f"line {100 - cr._LOG_TAIL_LINES}" + assert cr.take_previous_crash() is None # only offered once + # This session's dump file starts empty. + assert (tmp_path / cr._NATIVE_LOG_NAME).read_text(encoding="utf-8") == "" + + def test_running_second_instance_is_not_a_crash(self, tmp_path): + from PySide6.QtCore import QLockFile + other = QLockFile(str(tmp_path / cr._SESSION_LOCK_NAME)) + assert other.tryLock(0) + try: + assert cr._detect_previous_crash(tmp_path) is None + assert cr._session_lock is None + finally: + other.unlock() + + def test_show_previous_crash_notice_without_crash_does_nothing(self, monkeypatch): + import opensak.gui.dialogs.crash_report_dialog as dlg_mod + monkeypatch.setattr(dlg_mod, "CrashReportDialog", None) # would raise if used + cr.show_previous_crash_notice() + + +# ── Hooks ──────────────────────────────────────────────────────────────────── + +class TestHooks: + def test_install_and_shutdown_swap_hooks(self, tmp_path): + cr.install(tmp_path) + assert sys.excepthook is cr._excepthook + assert threading.excepthook is cr._thread_excepthook + cr.shutdown() + assert sys.excepthook is sys.__excepthook__ + + def test_excepthook_logs_and_shows_report(self, tmp_path, monkeypatch, qtbot, caplog): + shown = [] + monkeypatch.setattr(cr, "_show_exception_dialog", shown.append) + cr.install(tmp_path) + sys.excepthook(*_raise_and_capture(ValueError("from gui"))) + assert len(shown) == 1 and shown[0].endswith("ValueError: from gui") + assert "Uncaught exception" in caplog.text + + def test_worker_thread_exception_reaches_gui_thread(self, tmp_path, monkeypatch, qtbot): + shown = [] + monkeypatch.setattr( + cr, "_show_exception_dialog", + lambda r: shown.append((r, threading.current_thread() is threading.main_thread())), + ) + cr.install(tmp_path) + + def worker(): + raise RuntimeError("from worker") + + t = threading.Thread(target=worker) + t.start() + t.join() + qtbot.waitUntil(lambda: bool(shown), timeout=2000) + report, on_main = shown[0] + assert report.endswith("RuntimeError: from worker") + assert on_main + + def test_keyboard_interrupt_goes_to_default_hook(self, tmp_path, monkeypatch): + called = [] + monkeypatch.setattr(sys, "__excepthook__", lambda *a: called.append(a[0])) + monkeypatch.setattr(cr, "_show_exception_dialog", lambda r: pytest.fail("shown")) + cr.install(tmp_path) + sys.excepthook(*_raise_and_capture(KeyboardInterrupt())) + assert called == [KeyboardInterrupt] + + def test_same_error_opens_dialog_once(self, monkeypatch): + import opensak.gui.dialogs.crash_report_dialog as dlg_mod + opened = [] + + class FakeDialog: + quit_requested = False + + def __init__(self, report, previous_session, parent=None): + opened.append(report) + + def exec(self): + return 0 + + monkeypatch.setattr(dlg_mod, "CrashReportDialog", FakeDialog) + cr._show_exception_dialog("Traceback\nValueError: a") + cr._show_exception_dialog("Traceback\nValueError: a") + cr._show_exception_dialog("Traceback\nValueError: b") + assert len(opened) == 2 + + +# ── Dialog ─────────────────────────────────────────────────────────────────── + +class TestDialog: + @pytest.fixture + def opened(self, monkeypatch): + import opensak.gui.dialogs.crash_report_dialog as dlg_mod + urls = [] + monkeypatch.setattr(dlg_mod.QDesktopServices, "openUrl", lambda u: urls.append(u.toString())) + return urls + + def _make(self, qtbot, previous_session=False): + from opensak.gui.dialogs.crash_report_dialog import CrashReportDialog + dlg = CrashReportDialog("Traceback\nValueError: x", previous_session=previous_session) + qtbot.addWidget(dlg) + return dlg + + def test_shows_report_read_only(self, qtbot): + dlg = self._make(qtbot) + assert dlg._text.toPlainText() == "Traceback\nValueError: x" + assert dlg._text.isReadOnly() + + def test_github_opens_issue_and_log_folder(self, qtbot, opened): + dlg = self._make(qtbot) + dlg._btn_github.click() + assert opened[0].startswith(cr.GITHUB_NEW_ISSUE_URL) + assert opened[1].startswith("file:") + + def test_email_opens_mailto(self, qtbot, opened): + dlg = self._make(qtbot) + dlg._btn_email.click() + assert opened[0].startswith("mailto:hello@opensak.com") + + def test_copy_puts_report_on_clipboard(self, qtbot): + from PySide6.QtWidgets import QApplication + dlg = self._make(qtbot) + dlg._btn_copy.click() + assert QApplication.clipboard().text() == "Traceback\nValueError: x" + + def test_quit_sets_flag(self, qtbot): + dlg = self._make(qtbot) + dlg._btn_quit.click() + assert dlg.quit_requested + + def test_previous_session_has_no_quit_button(self, qtbot): + dlg = self._make(qtbot, previous_session=True) + assert not hasattr(dlg, "_btn_quit") From 36e36c24851df174a6acf236d2a7e9380a997787 Mon Sep 17 00:00:00 2001 From: nagisml Date: Sat, 17 Oct 2026 07:16:00 +0200 Subject: [PATCH 2/8] adda FB as an reporting option --- src/opensak/crash_report.py | 5 +++-- src/opensak/gui/dialogs/crash_report_dialog.py | 15 +++++++++++++-- src/opensak/lang/cs.py | 7 ++++--- src/opensak/lang/da.py | 7 ++++--- src/opensak/lang/de.py | 7 ++++--- src/opensak/lang/de_CH.py | 7 ++++--- src/opensak/lang/en.py | 7 ++++--- src/opensak/lang/es.py | 7 ++++--- src/opensak/lang/fr.py | 7 ++++--- src/opensak/lang/nl.py | 7 ++++--- src/opensak/lang/pl.py | 7 ++++--- src/opensak/lang/pt.py | 7 ++++--- src/opensak/lang/se.py | 7 ++++--- tests/unit-tests/test_crash_report.py | 8 ++++++++ 14 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/opensak/crash_report.py b/src/opensak/crash_report.py index d8a80cdf..752155d4 100644 --- a/src/opensak/crash_report.py +++ b/src/opensak/crash_report.py @@ -16,8 +16,8 @@ session's log (opensak.log.previous, issue #737). Nothing is ever sent automatically: the report goes to a prefilled GitHub -issue or e-mail only when the user clicks the button, and the text is -scrubbed of home paths, the user name and tokens first. +issue or e-mail, or is copied for a post in the Facebook group, only when +the user clicks the button, and the text is scrubbed of home paths, the user name and tokens first. """ from __future__ import annotations @@ -44,6 +44,7 @@ GITHUB_NEW_ISSUE_URL = "https://github.com/OpenSAK-Org/OpenSAK/issues/new" SUPPORT_EMAIL = "hello@opensak.com" +FACEBOOK_GROUP_URL = "https://www.facebook.com/groups/2062154441232996" # GitHub refuses very long URLs (~8 KB); mail clients choke much earlier # (Outlook/Windows mailto handlers at ~2 KB). The body is cut to fit. diff --git a/src/opensak/gui/dialogs/crash_report_dialog.py b/src/opensak/gui/dialogs/crash_report_dialog.py index 9709a4d0..962e19b5 100644 --- a/src/opensak/gui/dialogs/crash_report_dialog.py +++ b/src/opensak/gui/dialogs/crash_report_dialog.py @@ -5,7 +5,9 @@ Shown for an uncaught exception, and at startup when the previous session did not exit cleanly. The scrubbed report is shown read-only so the user sees exactly what would be shared; nothing leaves the machine until they -click GitHub or e-mail, and both only open a prefilled page/mail. +click GitHub, Facebook or e-mail. GitHub and e-mail open a prefilled +page/mail; Facebook cannot be prefilled, so the report is copied to the +clipboard and the group is opened to paste it into a post. """ from __future__ import annotations @@ -66,13 +68,15 @@ def _setup_ui(self) -> None: report_row = QHBoxLayout() self._btn_github = QPushButton(tr("crash_dialog_github")) self._btn_github.clicked.connect(self._open_github) + self._btn_facebook = QPushButton(tr("crash_dialog_facebook")) + self._btn_facebook.clicked.connect(self._open_facebook) self._btn_email = QPushButton(tr("crash_dialog_email")) self._btn_email.clicked.connect(self._open_email) self._btn_copy = QPushButton(tr("crash_dialog_copy")) self._btn_copy.clicked.connect(self._copy) self._btn_log = QPushButton(tr("crash_dialog_show_log")) self._btn_log.clicked.connect(self._show_log) - for btn in (self._btn_github, self._btn_email, self._btn_copy, self._btn_log): + for btn in (self._btn_github, self._btn_facebook, self._btn_email, self._btn_copy, self._btn_log): report_row.addWidget(btn) layout.addLayout(report_row) @@ -99,6 +103,13 @@ def _open_github(self) -> None: # The issue page cannot take the log via URL — show it to drag in. self._show_log() + def _open_facebook(self) -> None: + # A group post cannot be prefilled via URL — copy the report so it + # can be pasted, then open the group and the log folder. + self._copy() + QDesktopServices.openUrl(QUrl(crash_report.FACEBOOK_GROUP_URL)) + self._show_log() + def _open_email(self) -> None: QDesktopServices.openUrl( QUrl(crash_report.mailto_url(self._report, self._previous_session)) diff --git a/src/opensak/lang/cs.py b/src/opensak/lang/cs.py index 951960ae..755aef08 100644 --- a/src/opensak/lang/cs.py +++ b/src/opensak/lang/cs.py @@ -1804,10 +1804,11 @@ "gsak_filter_import_failed_header": "{count} filtrů se nepodařilo převést:", "crash_dialog_title": "OpenSAK narazil na problém", - "crash_dialog_msg": "V OpenSAK došlo k neočekávané chybě. Můžete zkusit pokračovat, ale bezpečnější je uložit práci a restartovat.\n\nPomozte nám ji opravit: nahlaste ji na GitHubu nebo ji pošlete na {email} a přiložte soubor protokolu.", - "crash_dialog_previous_msg": "OpenSAK se při posledním spuštění neukončil normálně — mohl spadnout.\n\nPokud si pamatujete, co jste dělali, nahlaste to prosím na GitHubu nebo to pošlete na {email} a přiložte soubor protokolu.", - "crash_dialog_privacy": "Přesně toto bude sdíleno. Nic se neodesílá automaticky — tlačítka pouze otevřou předvyplněné hlášení na GitHubu nebo e-mail. Vaše domovská složka, uživatelské jméno a tokeny byly odstraněny.", + "crash_dialog_msg": "V OpenSAK došlo k neočekávané chybě. Můžete zkusit pokračovat, ale bezpečnější je uložit práci a restartovat.\n\nPomozte nám ji opravit: nahlaste ji na GitHubu nebo v naší skupině na Facebooku, nebo ji pošlete na {email} a přiložte soubor protokolu.", + "crash_dialog_previous_msg": "OpenSAK se při posledním spuštění neukončil normálně — mohl spadnout.\n\nPokud si pamatujete, co jste dělali, nahlaste to prosím na GitHubu nebo v naší skupině na Facebooku, nebo to pošlete na {email} a přiložte soubor protokolu.", + "crash_dialog_privacy": "Přesně toto bude sdíleno. Nic se neodesílá automaticky — tlačítka pouze otevřou předvyplněné hlášení na GitHubu nebo e-mail, případně zkopírují hlášení a otevřou skupinu na Facebooku, kam ho vložíte. Vaše domovská složka, uživatelské jméno a tokeny byly odstraněny.", "crash_dialog_github": "Nahlásit na GitHubu", + "crash_dialog_facebook": "Sdílet ve skupině na Facebooku", "crash_dialog_email": "Odeslat e-mailem", "crash_dialog_copy": "Kopírovat hlášení", "crash_dialog_copied": "✓ Zkopírováno", diff --git a/src/opensak/lang/da.py b/src/opensak/lang/da.py index 3e838ff4..734a78d6 100644 --- a/src/opensak/lang/da.py +++ b/src/opensak/lang/da.py @@ -1808,10 +1808,11 @@ "gsak_filter_import_failed_header": "{count} filtre kunne ikke konverteres:", "crash_dialog_title": "OpenSAK stødte på et problem", - "crash_dialog_msg": "OpenSAK ramte en uventet fejl. Du kan prøve at fortsætte, men det er sikrest at gemme dit arbejde og genstarte.\n\nHjælp os med at rette den: rapportér den på GitHub eller send den til {email}, og vedhæft logfilen.", - "crash_dialog_previous_msg": "OpenSAK blev ikke lukket normalt sidst den kørte — den kan være gået ned.\n\nHvis du husker, hvad du lavede, så rapportér det venligst på GitHub eller send det til {email}, og vedhæft logfilen.", - "crash_dialog_privacy": "Præcis dette bliver delt. Intet sendes automatisk — knapperne åbner kun en udfyldt GitHub-sag eller e-mail. Din hjemmemappe, dit brugernavn og tokens er fjernet.", + "crash_dialog_msg": "OpenSAK ramte en uventet fejl. Du kan prøve at fortsætte, men det er sikrest at gemme dit arbejde og genstarte.\n\nHjælp os med at rette den: rapportér den på GitHub eller i vores Facebook-gruppe, eller send den til {email}, og vedhæft logfilen.", + "crash_dialog_previous_msg": "OpenSAK blev ikke lukket normalt sidst den kørte — den kan være gået ned.\n\nHvis du husker, hvad du lavede, så rapportér det venligst på GitHub eller i vores Facebook-gruppe, eller send det til {email}, og vedhæft logfilen.", + "crash_dialog_privacy": "Præcis dette bliver delt. Intet sendes automatisk — knapperne åbner kun en udfyldt GitHub-sag eller e-mail, eller kopierer rapporten og åbner Facebook-gruppen, så du kan indsætte den. Din hjemmemappe, dit brugernavn og tokens er fjernet.", "crash_dialog_github": "Rapportér på GitHub", + "crash_dialog_facebook": "Del i Facebook-gruppen", "crash_dialog_email": "Send via e-mail", "crash_dialog_copy": "Kopiér rapport", "crash_dialog_copied": "✓ Kopieret", diff --git a/src/opensak/lang/de.py b/src/opensak/lang/de.py index 508619b3..3841248e 100644 --- a/src/opensak/lang/de.py +++ b/src/opensak/lang/de.py @@ -1809,10 +1809,11 @@ "gsak_filter_import_failed_header": "{count} Filter konnten nicht umgewandelt werden:", "crash_dialog_title": "OpenSAK ist auf ein Problem gestoßen", - "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", + "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder in unserer Facebook-Gruppe oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder in unserer Facebook-Gruppe oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail bzw. kopieren den Bericht und öffnen die Facebook-Gruppe zum Einfügen. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", "crash_dialog_github": "Auf GitHub melden", + "crash_dialog_facebook": "In Facebook-Gruppe posten", "crash_dialog_email": "Per E-Mail senden", "crash_dialog_copy": "Bericht kopieren", "crash_dialog_copied": "✓ Kopiert", diff --git a/src/opensak/lang/de_CH.py b/src/opensak/lang/de_CH.py index ff36770d..e4f6416f 100644 --- a/src/opensak/lang/de_CH.py +++ b/src/opensak/lang/de_CH.py @@ -1810,10 +1810,11 @@ "gsak_filter_import_failed_header": "{count} Filter konnten nicht umgewandelt werden:", "crash_dialog_title": "OpenSAK ist auf ein Problem gestossen", - "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", + "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder in unserer Facebook-Gruppe oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder in unserer Facebook-Gruppe oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail bzw. kopieren den Bericht und öffnen die Facebook-Gruppe zum Einfügen. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", "crash_dialog_github": "Auf GitHub melden", + "crash_dialog_facebook": "In Facebook-Gruppe posten", "crash_dialog_email": "Per E-Mail senden", "crash_dialog_copy": "Bericht kopieren", "crash_dialog_copied": "✓ Kopiert", diff --git a/src/opensak/lang/en.py b/src/opensak/lang/en.py index 59aa7598..5ce22120 100644 --- a/src/opensak/lang/en.py +++ b/src/opensak/lang/en.py @@ -1807,10 +1807,11 @@ "gsak_filter_import_failed_header": "{count} filter(s) could not be converted:", "crash_dialog_title": "OpenSAK ran into a problem", - "crash_dialog_msg": "OpenSAK hit an unexpected error. You can try to continue, but it is safer to save your work and restart.\n\nPlease help us fix it: report it on GitHub or send it to {email}, and attach the log file.", - "crash_dialog_previous_msg": "OpenSAK did not close normally the last time it ran — it may have crashed.\n\nIf you remember what you were doing, please report it on GitHub or send it to {email}, and attach the log file.", - "crash_dialog_privacy": "This is exactly what will be shared. Nothing is sent automatically — the buttons only open a prefilled GitHub issue or e-mail. Your home folder, user name and tokens have been removed.", + "crash_dialog_msg": "OpenSAK hit an unexpected error. You can try to continue, but it is safer to save your work and restart.\n\nPlease help us fix it: report it on GitHub or in our Facebook group, or send it to {email}, and attach the log file.", + "crash_dialog_previous_msg": "OpenSAK did not close normally the last time it ran — it may have crashed.\n\nIf you remember what you were doing, please report it on GitHub or in our Facebook group, or send it to {email}, and attach the log file.", + "crash_dialog_privacy": "This is exactly what will be shared. Nothing is sent automatically — the buttons only open a prefilled GitHub issue or e-mail, or copy the report and open the Facebook group to paste it. Your home folder, user name and tokens have been removed.", "crash_dialog_github": "Report on GitHub", + "crash_dialog_facebook": "Post in Facebook group", "crash_dialog_email": "Send by e-mail", "crash_dialog_copy": "Copy report", "crash_dialog_copied": "✓ Copied", diff --git a/src/opensak/lang/es.py b/src/opensak/lang/es.py index 4e55a53c..16696235 100644 --- a/src/opensak/lang/es.py +++ b/src/opensak/lang/es.py @@ -1809,10 +1809,11 @@ "gsak_filter_import_failed_header": "No se pudieron convertir {count} filtro(s):", "crash_dialog_title": "OpenSAK ha encontrado un problema", - "crash_dialog_msg": "OpenSAK ha sufrido un error inesperado. Puedes intentar continuar, pero es más seguro guardar tu trabajo y reiniciar.\n\nAyúdanos a corregirlo: infórmalo en GitHub o envíalo a {email}, y adjunta el archivo de registro.", - "crash_dialog_previous_msg": "OpenSAK no se cerró correctamente la última vez que se ejecutó; puede que se haya bloqueado.\n\nSi recuerdas lo que estabas haciendo, infórmalo en GitHub o envíalo a {email}, y adjunta el archivo de registro.", - "crash_dialog_privacy": "Esto es exactamente lo que se compartirá. No se envía nada automáticamente: los botones solo abren una incidencia de GitHub o un correo ya rellenados. Se han eliminado tu carpeta personal, tu nombre de usuario y los tokens.", + "crash_dialog_msg": "OpenSAK ha sufrido un error inesperado. Puedes intentar continuar, pero es más seguro guardar tu trabajo y reiniciar.\n\nAyúdanos a corregirlo: infórmalo en GitHub o en nuestro grupo de Facebook, o envíalo a {email}, y adjunta el archivo de registro.", + "crash_dialog_previous_msg": "OpenSAK no se cerró correctamente la última vez que se ejecutó; puede que se haya bloqueado.\n\nSi recuerdas lo que estabas haciendo, infórmalo en GitHub o en nuestro grupo de Facebook, o envíalo a {email}, y adjunta el archivo de registro.", + "crash_dialog_privacy": "Esto es exactamente lo que se compartirá. No se envía nada automáticamente: los botones solo abren una incidencia de GitHub o un correo ya rellenados, o copian el informe y abren el grupo de Facebook para pegarlo. Se han eliminado tu carpeta personal, tu nombre de usuario y los tokens.", "crash_dialog_github": "Informar en GitHub", + "crash_dialog_facebook": "Publicar en el grupo de Facebook", "crash_dialog_email": "Enviar por correo", "crash_dialog_copy": "Copiar informe", "crash_dialog_copied": "✓ Copiado", diff --git a/src/opensak/lang/fr.py b/src/opensak/lang/fr.py index 04625bc3..022538e6 100644 --- a/src/opensak/lang/fr.py +++ b/src/opensak/lang/fr.py @@ -1809,10 +1809,11 @@ "gsak_filter_import_failed_header": "{count} filtre(s) n'ont pas pu être convertis :", "crash_dialog_title": "OpenSAK a rencontré un problème", - "crash_dialog_msg": "OpenSAK a rencontré une erreur inattendue. Vous pouvez essayer de continuer, mais il est plus sûr d'enregistrer votre travail et de redémarrer.\n\nAidez-nous à la corriger : signalez-la sur GitHub ou envoyez-la à {email}, en joignant le fichier journal.", - "crash_dialog_previous_msg": "OpenSAK ne s'est pas fermé normalement lors de sa dernière exécution — il a peut-être planté.\n\nSi vous vous souvenez de ce que vous faisiez, signalez-le sur GitHub ou envoyez-le à {email}, en joignant le fichier journal.", - "crash_dialog_privacy": "C'est exactement ce qui sera partagé. Rien n'est envoyé automatiquement — les boutons ouvrent seulement un ticket GitHub ou un e-mail pré-rempli. Votre dossier personnel, votre nom d'utilisateur et vos jetons ont été supprimés.", + "crash_dialog_msg": "OpenSAK a rencontré une erreur inattendue. Vous pouvez essayer de continuer, mais il est plus sûr d'enregistrer votre travail et de redémarrer.\n\nAidez-nous à la corriger : signalez-la sur GitHub ou dans notre groupe Facebook, ou envoyez-la à {email}, en joignant le fichier journal.", + "crash_dialog_previous_msg": "OpenSAK ne s'est pas fermé normalement lors de sa dernière exécution — il a peut-être planté.\n\nSi vous vous souvenez de ce que vous faisiez, signalez-le sur GitHub ou dans notre groupe Facebook, ou envoyez-le à {email}, en joignant le fichier journal.", + "crash_dialog_privacy": "C'est exactement ce qui sera partagé. Rien n'est envoyé automatiquement — les boutons ouvrent seulement un ticket GitHub ou un e-mail pré-rempli, ou copient le rapport et ouvrent le groupe Facebook pour le coller. Votre dossier personnel, votre nom d'utilisateur et vos jetons ont été supprimés.", "crash_dialog_github": "Signaler sur GitHub", + "crash_dialog_facebook": "Publier dans le groupe Facebook", "crash_dialog_email": "Envoyer par e-mail", "crash_dialog_copy": "Copier le rapport", "crash_dialog_copied": "✓ Copié", diff --git a/src/opensak/lang/nl.py b/src/opensak/lang/nl.py index 83a1a6b9..84f127c8 100644 --- a/src/opensak/lang/nl.py +++ b/src/opensak/lang/nl.py @@ -1806,10 +1806,11 @@ "gsak_filter_import_failed_header": "{count} filter(s) konden niet omgezet worden:", "crash_dialog_title": "OpenSAK is tegen een probleem aangelopen", - "crash_dialog_msg": "Er is een onverwachte fout opgetreden in OpenSAK. U kunt proberen door te gaan, maar het is veiliger om uw werk op te slaan en opnieuw te starten.\n\nHelp ons de fout op te lossen: meld deze op GitHub of stuur deze naar {email}, en voeg het logbestand toe.", - "crash_dialog_previous_msg": "OpenSAK is de vorige keer niet normaal afgesloten — mogelijk is het gecrasht.\n\nAls u nog weet wat u aan het doen was, meld het dan op GitHub of stuur het naar {email}, en voeg het logbestand toe.", - "crash_dialog_privacy": "Precies dit wordt gedeeld. Er wordt niets automatisch verzonden — de knoppen openen alleen een vooraf ingevuld GitHub-issue of e-mail. Uw thuismap, gebruikersnaam en tokens zijn verwijderd.", + "crash_dialog_msg": "Er is een onverwachte fout opgetreden in OpenSAK. U kunt proberen door te gaan, maar het is veiliger om uw werk op te slaan en opnieuw te starten.\n\nHelp ons de fout op te lossen: meld deze op GitHub of in onze Facebook-groep, of stuur deze naar {email}, en voeg het logbestand toe.", + "crash_dialog_previous_msg": "OpenSAK is de vorige keer niet normaal afgesloten — mogelijk is het gecrasht.\n\nAls u nog weet wat u aan het doen was, meld het dan op GitHub of in onze Facebook-groep, of stuur het naar {email}, en voeg het logbestand toe.", + "crash_dialog_privacy": "Precies dit wordt gedeeld. Er wordt niets automatisch verzonden — de knoppen openen alleen een vooraf ingevuld GitHub-issue of e-mail, of kopiëren het rapport en openen de Facebook-groep om het te plakken. Uw thuismap, gebruikersnaam en tokens zijn verwijderd.", "crash_dialog_github": "Melden op GitHub", + "crash_dialog_facebook": "Plaatsen in Facebook-groep", "crash_dialog_email": "Versturen per e-mail", "crash_dialog_copy": "Rapport kopiëren", "crash_dialog_copied": "✓ Gekopieerd", diff --git a/src/opensak/lang/pl.py b/src/opensak/lang/pl.py index 85fd1d11..b806953f 100644 --- a/src/opensak/lang/pl.py +++ b/src/opensak/lang/pl.py @@ -1809,10 +1809,11 @@ "gsak_filter_import_failed_header": "Nie udało się przekonwertować {count} filtrów:", "crash_dialog_title": "OpenSAK napotkał problem", - "crash_dialog_msg": "W OpenSAK wystąpił nieoczekiwany błąd. Możesz spróbować kontynuować, ale bezpieczniej jest zapisać pracę i uruchomić program ponownie.\n\nPomóż nam go naprawić: zgłoś go na GitHubie lub wyślij na {email} i dołącz plik dziennika.", - "crash_dialog_previous_msg": "OpenSAK nie został poprawnie zamknięty podczas ostatniego uruchomienia — mógł ulec awarii.\n\nJeśli pamiętasz, co robiłeś, zgłoś to na GitHubie lub wyślij na {email} i dołącz plik dziennika.", - "crash_dialog_privacy": "Dokładnie to zostanie udostępnione. Nic nie jest wysyłane automatycznie — przyciski otwierają jedynie wstępnie wypełnione zgłoszenie na GitHubie lub e-mail. Twój folder domowy, nazwa użytkownika i tokeny zostały usunięte.", + "crash_dialog_msg": "W OpenSAK wystąpił nieoczekiwany błąd. Możesz spróbować kontynuować, ale bezpieczniej jest zapisać pracę i uruchomić program ponownie.\n\nPomóż nam go naprawić: zgłoś go na GitHubie lub w naszej grupie na Facebooku albo wyślij na {email} i dołącz plik dziennika.", + "crash_dialog_previous_msg": "OpenSAK nie został poprawnie zamknięty podczas ostatniego uruchomienia — mógł ulec awarii.\n\nJeśli pamiętasz, co robiłeś, zgłoś to na GitHubie lub w naszej grupie na Facebooku albo wyślij na {email} i dołącz plik dziennika.", + "crash_dialog_privacy": "Dokładnie to zostanie udostępnione. Nic nie jest wysyłane automatycznie — przyciski otwierają jedynie wstępnie wypełnione zgłoszenie na GitHubie lub e-mail albo kopiują raport i otwierają grupę na Facebooku, aby go wkleić. Twój folder domowy, nazwa użytkownika i tokeny zostały usunięte.", "crash_dialog_github": "Zgłoś na GitHubie", + "crash_dialog_facebook": "Opublikuj w grupie na Facebooku", "crash_dialog_email": "Wyślij e-mailem", "crash_dialog_copy": "Kopiuj raport", "crash_dialog_copied": "✓ Skopiowano", diff --git a/src/opensak/lang/pt.py b/src/opensak/lang/pt.py index a16822e3..52001c85 100644 --- a/src/opensak/lang/pt.py +++ b/src/opensak/lang/pt.py @@ -1809,10 +1809,11 @@ "gsak_filter_import_failed_header": "{count} filtro(s) não puderam ser convertidos:", "crash_dialog_title": "O OpenSAK encontrou um problema", - "crash_dialog_msg": "O OpenSAK encontrou um erro inesperado. Pode tentar continuar, mas é mais seguro guardar o seu trabalho e reiniciar.\n\nAjude-nos a corrigi-lo: reporte-o no GitHub ou envie-o para {email}, e anexe o ficheiro de registo.", - "crash_dialog_previous_msg": "O OpenSAK não foi fechado normalmente da última vez que foi executado — pode ter falhado.\n\nSe se lembrar do que estava a fazer, reporte-o no GitHub ou envie-o para {email}, e anexe o ficheiro de registo.", - "crash_dialog_privacy": "É exatamente isto que será partilhado. Nada é enviado automaticamente — os botões apenas abrem um issue do GitHub ou um e-mail pré-preenchido. A sua pasta pessoal, nome de utilizador e tokens foram removidos.", + "crash_dialog_msg": "O OpenSAK encontrou um erro inesperado. Pode tentar continuar, mas é mais seguro guardar o seu trabalho e reiniciar.\n\nAjude-nos a corrigi-lo: reporte-o no GitHub ou no nosso grupo do Facebook, ou envie-o para {email}, e anexe o ficheiro de registo.", + "crash_dialog_previous_msg": "O OpenSAK não foi fechado normalmente da última vez que foi executado — pode ter falhado.\n\nSe se lembrar do que estava a fazer, reporte-o no GitHub ou no nosso grupo do Facebook, ou envie-o para {email}, e anexe o ficheiro de registo.", + "crash_dialog_privacy": "É exatamente isto que será partilhado. Nada é enviado automaticamente — os botões apenas abrem um issue do GitHub ou um e-mail pré-preenchido, ou copiam o relatório e abrem o grupo do Facebook para o colar. A sua pasta pessoal, nome de utilizador e tokens foram removidos.", "crash_dialog_github": "Reportar no GitHub", + "crash_dialog_facebook": "Publicar no grupo do Facebook", "crash_dialog_email": "Enviar por e-mail", "crash_dialog_copy": "Copiar relatório", "crash_dialog_copied": "✓ Copiado", diff --git a/src/opensak/lang/se.py b/src/opensak/lang/se.py index 823f213a..3257cd2e 100644 --- a/src/opensak/lang/se.py +++ b/src/opensak/lang/se.py @@ -1808,10 +1808,11 @@ "gsak_filter_import_failed_header": "{count} filter kunde inte konverteras:", "crash_dialog_title": "OpenSAK stötte på ett problem", - "crash_dialog_msg": "Ett oväntat fel uppstod i OpenSAK. Du kan försöka fortsätta, men det är säkrare att spara ditt arbete och starta om.\n\nHjälp oss att rätta det: rapportera det på GitHub eller skicka det till {email}, och bifoga loggfilen.", - "crash_dialog_previous_msg": "OpenSAK stängdes inte normalt senast det kördes — det kan ha kraschat.\n\nOm du minns vad du gjorde, rapportera det på GitHub eller skicka det till {email}, och bifoga loggfilen.", - "crash_dialog_privacy": "Exakt detta kommer att delas. Inget skickas automatiskt — knapparna öppnar bara ett förifyllt GitHub-ärende eller e-postmeddelande. Din hemmapp, ditt användarnamn och tokens har tagits bort.", + "crash_dialog_msg": "Ett oväntat fel uppstod i OpenSAK. Du kan försöka fortsätta, men det är säkrare att spara ditt arbete och starta om.\n\nHjälp oss att rätta det: rapportera det på GitHub eller i vår Facebook-grupp, eller skicka det till {email}, och bifoga loggfilen.", + "crash_dialog_previous_msg": "OpenSAK stängdes inte normalt senast det kördes — det kan ha kraschat.\n\nOm du minns vad du gjorde, rapportera det på GitHub eller i vår Facebook-grupp, eller skicka det till {email}, och bifoga loggfilen.", + "crash_dialog_privacy": "Exakt detta kommer att delas. Inget skickas automatiskt — knapparna öppnar bara ett förifyllt GitHub-ärende eller e-postmeddelande, eller kopierar rapporten och öppnar Facebook-gruppen så att du kan klistra in den. Din hemmapp, ditt användarnamn och tokens har tagits bort.", "crash_dialog_github": "Rapportera på GitHub", + "crash_dialog_facebook": "Posta i Facebook-gruppen", "crash_dialog_email": "Skicka via e-post", "crash_dialog_copy": "Kopiera rapport", "crash_dialog_copied": "✓ Kopierat", diff --git a/tests/unit-tests/test_crash_report.py b/tests/unit-tests/test_crash_report.py index 3e608792..dd9d1698 100644 --- a/tests/unit-tests/test_crash_report.py +++ b/tests/unit-tests/test_crash_report.py @@ -270,6 +270,14 @@ def test_github_opens_issue_and_log_folder(self, qtbot, opened): assert opened[0].startswith(cr.GITHUB_NEW_ISSUE_URL) assert opened[1].startswith("file:") + def test_facebook_copies_report_and_opens_group(self, qtbot, opened): + from PySide6.QtWidgets import QApplication + dlg = self._make(qtbot) + dlg._btn_facebook.click() + assert QApplication.clipboard().text() == "Traceback\nValueError: x" + assert opened[0] == cr.FACEBOOK_GROUP_URL + assert opened[1].startswith("file:") + def test_email_opens_mailto(self, qtbot, opened): dlg = self._make(qtbot) dlg._btn_email.click() From 36cf1174138881a174f3be540d0eb878b6507401 Mon Sep 17 00:00:00 2001 From: nagisml Date: Sun, 18 Oct 2026 13:49:24 +0200 Subject: [PATCH 3/8] Fix Dialog runs inside the failing code --- src/opensak/crash_report.py | 22 ++++++++++++++++------ tests/unit-tests/test_crash_report.py | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/opensak/crash_report.py b/src/opensak/crash_report.py index 752155d4..1035a3d9 100644 --- a/src/opensak/crash_report.py +++ b/src/opensak/crash_report.py @@ -285,8 +285,18 @@ def _handle(exc_type, exc_value, exc_tb) -> None: # Never let the reporter itself raise out of an excepthook. sys.__excepthook__(exc_type, exc_value, exc_tb) return - if _bridge is not None: - # A queued signal when called from a worker thread, direct otherwise. + if _bridge is None: + return + from PySide6.QtCore import QThread + if _bridge.thread() is QThread.currentThread() and QThread.currentThread().loopLevel() == 0: + # GUI thread with no event loop yet (startup before app.exec(), or an + # exception escaping main()): a queued signal would never be + # delivered, so show the dialog right here — nothing to re-enter. + _show_exception_dialog(report) + else: + # Always queued, also on the GUI thread: the excepthook runs while the + # failing slot / paint event is still on the stack, and dlg.exec() + # there would start a nested event loop inside it. _bridge.crashed.emit(report) @@ -331,17 +341,17 @@ def install(app_dir: Path | None = None) -> None: logger.warning("crash: unclean-shutdown check failed", exc_info=True) _enable_faulthandler(app_dir) - from PySide6.QtCore import QObject, Signal, Slot + from PySide6.QtCore import QObject, Qt, Signal, Slot class _Bridge(QObject): # The slot is a method of this GUI-thread object (not a plain - # function), so an emit from a worker thread is queued to the GUI - # thread instead of running the dialog in the worker. + # function), so the queued emit is delivered on the GUI thread even + # when it comes from a worker thread. crashed = Signal(str) def __init__(self) -> None: super().__init__() - self.crashed.connect(self._on_crashed) + self.crashed.connect(self._on_crashed, Qt.ConnectionType.QueuedConnection) @Slot(str) def _on_crashed(self, report: str) -> None: diff --git a/tests/unit-tests/test_crash_report.py b/tests/unit-tests/test_crash_report.py index dd9d1698..126b3e2e 100644 --- a/tests/unit-tests/test_crash_report.py +++ b/tests/unit-tests/test_crash_report.py @@ -196,6 +196,24 @@ def test_excepthook_logs_and_shows_report(self, tmp_path, monkeypatch, qtbot, ca assert len(shown) == 1 and shown[0].endswith("ValueError: from gui") assert "Uncaught exception" in caplog.text + def test_gui_exception_dialog_waits_for_handler_to_unwind(self, tmp_path, monkeypatch, qtbot): + from PySide6.QtCore import QEventLoop, QTimer + shown = [] + monkeypatch.setattr(cr, "_show_exception_dialog", shown.append) + cr.install(tmp_path) + during_handler = [] + + def failing_handler(): + sys.excepthook(*_raise_and_capture(ValueError("in slot"))) + during_handler.append(len(shown)) + + loop = QEventLoop() + QTimer.singleShot(0, failing_handler) + QTimer.singleShot(50, loop.quit) + loop.exec() + assert during_handler == [0] + assert len(shown) == 1 and shown[0].endswith("ValueError: in slot") + def test_worker_thread_exception_reaches_gui_thread(self, tmp_path, monkeypatch, qtbot): shown = [] monkeypatch.setattr( From 277bad6e0bcf2897730cf8a4a04d65c8049b1351 Mon Sep 17 00:00:00 2001 From: nagisml Date: Sun, 18 Oct 2026 13:54:37 +0200 Subject: [PATCH 4/8] Fix A second instance truncates the first instance's native dump. --- src/opensak/crash_report.py | 7 ++++++- tests/unit-tests/test_crash_report.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/opensak/crash_report.py b/src/opensak/crash_report.py index 1035a3d9..e3f4a33f 100644 --- a/src/opensak/crash_report.py +++ b/src/opensak/crash_report.py @@ -339,7 +339,12 @@ def install(app_dir: Path | None = None) -> None: _previous_crash = _detect_previous_crash(app_dir) except Exception: logger.warning("crash: unclean-shutdown check failed", exc_info=True) - _enable_faulthandler(app_dir) + # The native dump file belongs to the lock holder: without the lock, + # another instance's faulthandler may be writing to it, and opening it + # here would truncate that dump. Our own dump would not be read anyway — + # only the next lock holder looks at the file. + if _session_lock is not None: + _enable_faulthandler(app_dir) from PySide6.QtCore import QObject, Qt, Signal, Slot diff --git a/tests/unit-tests/test_crash_report.py b/tests/unit-tests/test_crash_report.py index 126b3e2e..e19ac21c 100644 --- a/tests/unit-tests/test_crash_report.py +++ b/tests/unit-tests/test_crash_report.py @@ -172,6 +172,19 @@ def test_running_second_instance_is_not_a_crash(self, tmp_path): finally: other.unlock() + def test_second_instance_leaves_native_dump_alone(self, tmp_path): + from PySide6.QtCore import QLockFile + other = QLockFile(str(tmp_path / cr._SESSION_LOCK_NAME)) + assert other.tryLock(0) + native = tmp_path / cr._NATIVE_LOG_NAME + native.write_text("Fatal Python error: first instance", encoding="utf-8") + try: + cr.install(tmp_path) + assert cr._native_file is None + assert native.read_text(encoding="utf-8") == "Fatal Python error: first instance" + finally: + other.unlock() + def test_show_previous_crash_notice_without_crash_does_nothing(self, monkeypatch): import opensak.gui.dialogs.crash_report_dialog as dlg_mod monkeypatch.setattr(dlg_mod, "CrashReportDialog", None) # would raise if used From b05006cda317145f89b10a7138ec24e00bd1a4e1 Mon Sep 17 00:00:00 2001 From: nagisml Date: Sun, 18 Oct 2026 14:00:17 +0200 Subject: [PATCH 5/8] Fix False "may have crashed" notices. --- src/opensak/crash_report.py | 19 +++++++++++++++---- tests/unit-tests/test_crash_report.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/opensak/crash_report.py b/src/opensak/crash_report.py index e3f4a33f..6d07e7e9 100644 --- a/src/opensak/crash_report.py +++ b/src/opensak/crash_report.py @@ -12,8 +12,11 @@ process being killed). faulthandler writes the native stack to crash_native.log, and a session lock file that is only removed on a clean exit tells the next start that the last session did not end - normally — the user is then offered to report it with the previous - session's log (opensak.log.previous, issue #737). + normally. If faulthandler left a dump, the user is offered to report it + with the previous session's log (opensak.log.previous, issue #737). + A lock without a dump (log-off/shutdown with OpenSAK open, a kill from + Task Manager, Force Quit, power loss) is only logged — those are not + OpenSAK bugs, and a dialog for each would teach users to dismiss it. Nothing is ever sent automatically: the report goes to a prefilled GitHub issue or e-mail, or is copied for a post in the Facebook group, only when @@ -224,7 +227,9 @@ def _detect_previous_crash(app_dir: Path) -> PreviousCrash | None: QLockFile stores the owning PID: if the file is still there but tryLock() succeeds, its owner is gone without unlocking — it crashed or was killed. If tryLock() fails another OpenSAK is running right now, which is not a - crash (and this instance then runs without a lock of its own). + crash (and this instance then runs without a lock of its own). An + unclean exit without a native dump is logged but not reported: the + process was killed or the OS session ended, it did not crash. """ global _session_lock from PySide6.QtCore import QLockFile @@ -242,7 +247,13 @@ def _detect_previous_crash(app_dir: Path) -> PreviousCrash | None: from opensak.config import get_previous_log_path native = _read_tail(app_dir / _NATIVE_LOG_NAME, 200) - logger.warning("crash: previous session did not exit cleanly") + if not native.strip(): + logger.info( + "crash: previous session did not exit cleanly, but left no native " + "dump (killed, log-off/shutdown or power loss) — not reported" + ) + return None + logger.warning("crash: previous session crashed (native dump found)") return PreviousCrash( native_dump=native, log_tail=_read_tail(get_previous_log_path(), _LOG_TAIL_LINES), diff --git a/tests/unit-tests/test_crash_report.py b/tests/unit-tests/test_crash_report.py index e19ac21c..fcd2da4a 100644 --- a/tests/unit-tests/test_crash_report.py +++ b/tests/unit-tests/test_crash_report.py @@ -162,6 +162,17 @@ def test_lock_left_by_dead_process_is_a_crash(self, tmp_path): # This session's dump file starts empty. assert (tmp_path / cr._NATIVE_LOG_NAME).read_text(encoding="utf-8") == "" + @pytest.mark.parametrize("dump", [None, "", " \n"]) + def test_lock_without_native_dump_is_only_logged(self, tmp_path, caplog, dump): + # Killed, log-off/shutdown or power loss: the lock stays, no dump. + _leave_stale_lock(tmp_path / cr._SESSION_LOCK_NAME) + if dump is not None: + (tmp_path / cr._NATIVE_LOG_NAME).write_text(dump, encoding="utf-8") + caplog.set_level("INFO", logger="opensak.crash") + cr.install(tmp_path) + assert cr.take_previous_crash() is None + assert "left no native dump" in caplog.text + def test_running_second_instance_is_not_a_crash(self, tmp_path): from PySide6.QtCore import QLockFile other = QLockFile(str(tmp_path / cr._SESSION_LOCK_NAME)) From f90c288c6c1fabd9f013f5514da75193f922872a Mon Sep 17 00:00:00 2001 From: nagisml Date: Sun, 18 Oct 2026 14:04:03 +0200 Subject: [PATCH 6/8] Fix Stacked startup dialogs. --- src/opensak/app.py | 34 +++++++++++++++++++++++++--------- tests/unit-tests/test_app.py | 17 +++++++++++++++-- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/opensak/app.py b/src/opensak/app.py index d2855c81..790d00b6 100644 --- a/src/opensak/app.py +++ b/src/opensak/app.py @@ -464,17 +464,33 @@ def _close_splash(): from PySide6.QtCore import QTimer QTimer.singleShot(400, _close_splash) - # Issue #549: after the splash has gone, tell the user if the active - # database was backed up before being migrated during startup. - QTimer.singleShot(500, window.show_premigration_backup_notices) - # Issue #1045: the last active database was unavailable (e.g. a removed - # USB drive) and startup fell back to another one. - QTimer.singleShot(550, window.show_database_fallback_notice) + + # After the splash has gone, show the startup notices one after another. + # Each is modal, so separate timers would fire inside the first dialog's + # event loop and stack the others on top of it. + startup_notices = [ + # Issue #549: the active database was backed up before being + # migrated during startup. + window.show_premigration_backup_notices, + # Issue #1045: the last active database was unavailable (e.g. a + # removed USB drive) and startup fell back to another one. + window.show_database_fallback_notice, + ] if settings_restore_result is not None: - QTimer.singleShot( - 600, lambda: window.show_settings_restore_notice(settings_restore_result) + startup_notices.append( + lambda: window.show_settings_restore_notice(settings_restore_result) ) - QTimer.singleShot(650, lambda: crash_report.show_previous_crash_notice(window)) + startup_notices.append(lambda: crash_report.show_previous_crash_notice(window)) + + def _show_startup_notices(): + for notice in startup_notices: + try: + notice() + except Exception: + # Report it, but don't let one broken notice hide the rest. + sys.excepthook(*sys.exc_info()) + + QTimer.singleShot(500, _show_startup_notices) window.show() exit_code = app.exec() diff --git a/tests/unit-tests/test_app.py b/tests/unit-tests/test_app.py index c17d5b64..d5dc8b8a 100644 --- a/tests/unit-tests/test_app.py +++ b/tests/unit-tests/test_app.py @@ -144,16 +144,24 @@ def test_main_smoke(qapp, monkeypatch): monkeypatch.setattr("opensak.db.manager.get_db_manager", lambda: SimpleNamespace(ensure_active_initialised=lambda: None)) + notices = [] + class FakeWindow: def show(self): pass def show_premigration_backup_notices(self): # issue #549 - pass + notices.append("premigration") + raise RuntimeError("broken notice") def show_database_fallback_notice(self): # issue #1045 - pass + notices.append("fallback") monkeypatch.setattr("opensak.gui.mainwindow.MainWindow", FakeWindow) + from opensak import crash_report + reported = [] + monkeypatch.setattr(crash_report, "_show_exception_dialog", reported.append) + monkeypatch.setattr(crash_report, "show_previous_crash_notice", + lambda parent=None: notices.append("crash")) # Reuse the existing QApplication; restore the class *before* the test # returns so pytest-qt's post-call _process_events still sees the real one. @@ -167,3 +175,8 @@ def show_database_fallback_notice(self): # issue #1045 finally: W.QApplication = real_qapp_cls C.QTimer.singleShot = real_single + crash_report.shutdown() + # The startup notices run one after another from a single timer; a + # failing one is reported and doesn't hide the rest. + assert notices == ["premigration", "fallback", "crash"] + assert len(reported) == 1 and reported[0].endswith("RuntimeError: broken notice") From f0b8f2c7c4ade88f961b4ade666f64bdaacb6afd Mon Sep 17 00:00:00 2001 From: nagisml Date: Sun, 18 Oct 2026 14:10:38 +0200 Subject: [PATCH 7/8] adapted Reporting channels --- src/opensak/crash_report.py | 5 ++- .../gui/dialogs/crash_report_dialog.py | 33 ++++++++++--------- src/opensak/lang/cs.py | 7 ++-- src/opensak/lang/da.py | 7 ++-- src/opensak/lang/de.py | 7 ++-- src/opensak/lang/de_CH.py | 7 ++-- src/opensak/lang/en.py | 7 ++-- src/opensak/lang/es.py | 7 ++-- src/opensak/lang/fr.py | 7 ++-- src/opensak/lang/nl.py | 7 ++-- src/opensak/lang/pl.py | 7 ++-- src/opensak/lang/pt.py | 7 ++-- src/opensak/lang/se.py | 7 ++-- tests/unit-tests/test_crash_report.py | 10 +++--- 14 files changed, 56 insertions(+), 69 deletions(-) diff --git a/src/opensak/crash_report.py b/src/opensak/crash_report.py index 6d07e7e9..6da88d55 100644 --- a/src/opensak/crash_report.py +++ b/src/opensak/crash_report.py @@ -19,8 +19,8 @@ OpenSAK bugs, and a dialog for each would teach users to dismiss it. Nothing is ever sent automatically: the report goes to a prefilled GitHub -issue or e-mail, or is copied for a post in the Facebook group, only when -the user clicks the button, and the text is scrubbed of home paths, the user name and tokens first. +issue or e-mail only when the user clicks the button, and the text is +scrubbed of home paths, the user name and tokens first. """ from __future__ import annotations @@ -47,7 +47,6 @@ GITHUB_NEW_ISSUE_URL = "https://github.com/OpenSAK-Org/OpenSAK/issues/new" SUPPORT_EMAIL = "hello@opensak.com" -FACEBOOK_GROUP_URL = "https://www.facebook.com/groups/2062154441232996" # GitHub refuses very long URLs (~8 KB); mail clients choke much earlier # (Outlook/Windows mailto handlers at ~2 KB). The body is cut to fit. diff --git a/src/opensak/gui/dialogs/crash_report_dialog.py b/src/opensak/gui/dialogs/crash_report_dialog.py index 962e19b5..d70c71fa 100644 --- a/src/opensak/gui/dialogs/crash_report_dialog.py +++ b/src/opensak/gui/dialogs/crash_report_dialog.py @@ -5,14 +5,12 @@ Shown for an uncaught exception, and at startup when the previous session did not exit cleanly. The scrubbed report is shown read-only so the user sees exactly what would be shared; nothing leaves the machine until they -click GitHub, Facebook or e-mail. GitHub and e-mail open a prefilled -page/mail; Facebook cannot be prefilled, so the report is copied to the -clipboard and the group is opened to paste it into a post. +click GitHub or e-mail, which open a prefilled issue page / mail. """ from __future__ import annotations -from PySide6.QtCore import QUrl +from PySide6.QtCore import Qt, QUrl from PySide6.QtGui import QDesktopServices, QFontDatabase from PySide6.QtWidgets import ( QApplication, @@ -65,20 +63,30 @@ def _setup_ui(self) -> None: privacy.setStyleSheet("color: gray;") layout.addWidget(privacy) + # The two ways to report get full buttons; copying the report and + # opening the log folder are helpers, so they sit on a second row as + # flat buttons, which also leaves room for long translations. report_row = QHBoxLayout() self._btn_github = QPushButton(tr("crash_dialog_github")) self._btn_github.clicked.connect(self._open_github) - self._btn_facebook = QPushButton(tr("crash_dialog_facebook")) - self._btn_facebook.clicked.connect(self._open_facebook) self._btn_email = QPushButton(tr("crash_dialog_email")) self._btn_email.clicked.connect(self._open_email) + report_row.addWidget(self._btn_github) + report_row.addWidget(self._btn_email) + report_row.addStretch() + layout.addLayout(report_row) + + helper_row = QHBoxLayout() self._btn_copy = QPushButton(tr("crash_dialog_copy")) self._btn_copy.clicked.connect(self._copy) self._btn_log = QPushButton(tr("crash_dialog_show_log")) self._btn_log.clicked.connect(self._show_log) - for btn in (self._btn_github, self._btn_facebook, self._btn_email, self._btn_copy, self._btn_log): - report_row.addWidget(btn) - layout.addLayout(report_row) + for btn in (self._btn_copy, self._btn_log): + btn.setFlat(True) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + helper_row.addWidget(btn) + helper_row.addStretch() + layout.addLayout(helper_row) close_row = QHBoxLayout() close_row.addStretch() @@ -103,13 +111,6 @@ def _open_github(self) -> None: # The issue page cannot take the log via URL — show it to drag in. self._show_log() - def _open_facebook(self) -> None: - # A group post cannot be prefilled via URL — copy the report so it - # can be pasted, then open the group and the log folder. - self._copy() - QDesktopServices.openUrl(QUrl(crash_report.FACEBOOK_GROUP_URL)) - self._show_log() - def _open_email(self) -> None: QDesktopServices.openUrl( QUrl(crash_report.mailto_url(self._report, self._previous_session)) diff --git a/src/opensak/lang/cs.py b/src/opensak/lang/cs.py index 755aef08..951960ae 100644 --- a/src/opensak/lang/cs.py +++ b/src/opensak/lang/cs.py @@ -1804,11 +1804,10 @@ "gsak_filter_import_failed_header": "{count} filtrů se nepodařilo převést:", "crash_dialog_title": "OpenSAK narazil na problém", - "crash_dialog_msg": "V OpenSAK došlo k neočekávané chybě. Můžete zkusit pokračovat, ale bezpečnější je uložit práci a restartovat.\n\nPomozte nám ji opravit: nahlaste ji na GitHubu nebo v naší skupině na Facebooku, nebo ji pošlete na {email} a přiložte soubor protokolu.", - "crash_dialog_previous_msg": "OpenSAK se při posledním spuštění neukončil normálně — mohl spadnout.\n\nPokud si pamatujete, co jste dělali, nahlaste to prosím na GitHubu nebo v naší skupině na Facebooku, nebo to pošlete na {email} a přiložte soubor protokolu.", - "crash_dialog_privacy": "Přesně toto bude sdíleno. Nic se neodesílá automaticky — tlačítka pouze otevřou předvyplněné hlášení na GitHubu nebo e-mail, případně zkopírují hlášení a otevřou skupinu na Facebooku, kam ho vložíte. Vaše domovská složka, uživatelské jméno a tokeny byly odstraněny.", + "crash_dialog_msg": "V OpenSAK došlo k neočekávané chybě. Můžete zkusit pokračovat, ale bezpečnější je uložit práci a restartovat.\n\nPomozte nám ji opravit: nahlaste ji na GitHubu nebo ji pošlete na {email} a přiložte soubor protokolu.", + "crash_dialog_previous_msg": "OpenSAK se při posledním spuštění neukončil normálně — mohl spadnout.\n\nPokud si pamatujete, co jste dělali, nahlaste to prosím na GitHubu nebo to pošlete na {email} a přiložte soubor protokolu.", + "crash_dialog_privacy": "Přesně toto bude sdíleno. Nic se neodesílá automaticky — tlačítka pouze otevřou předvyplněné hlášení na GitHubu nebo e-mail. Vaše domovská složka, uživatelské jméno a tokeny byly odstraněny.", "crash_dialog_github": "Nahlásit na GitHubu", - "crash_dialog_facebook": "Sdílet ve skupině na Facebooku", "crash_dialog_email": "Odeslat e-mailem", "crash_dialog_copy": "Kopírovat hlášení", "crash_dialog_copied": "✓ Zkopírováno", diff --git a/src/opensak/lang/da.py b/src/opensak/lang/da.py index 734a78d6..3e838ff4 100644 --- a/src/opensak/lang/da.py +++ b/src/opensak/lang/da.py @@ -1808,11 +1808,10 @@ "gsak_filter_import_failed_header": "{count} filtre kunne ikke konverteres:", "crash_dialog_title": "OpenSAK stødte på et problem", - "crash_dialog_msg": "OpenSAK ramte en uventet fejl. Du kan prøve at fortsætte, men det er sikrest at gemme dit arbejde og genstarte.\n\nHjælp os med at rette den: rapportér den på GitHub eller i vores Facebook-gruppe, eller send den til {email}, og vedhæft logfilen.", - "crash_dialog_previous_msg": "OpenSAK blev ikke lukket normalt sidst den kørte — den kan være gået ned.\n\nHvis du husker, hvad du lavede, så rapportér det venligst på GitHub eller i vores Facebook-gruppe, eller send det til {email}, og vedhæft logfilen.", - "crash_dialog_privacy": "Præcis dette bliver delt. Intet sendes automatisk — knapperne åbner kun en udfyldt GitHub-sag eller e-mail, eller kopierer rapporten og åbner Facebook-gruppen, så du kan indsætte den. Din hjemmemappe, dit brugernavn og tokens er fjernet.", + "crash_dialog_msg": "OpenSAK ramte en uventet fejl. Du kan prøve at fortsætte, men det er sikrest at gemme dit arbejde og genstarte.\n\nHjælp os med at rette den: rapportér den på GitHub eller send den til {email}, og vedhæft logfilen.", + "crash_dialog_previous_msg": "OpenSAK blev ikke lukket normalt sidst den kørte — den kan være gået ned.\n\nHvis du husker, hvad du lavede, så rapportér det venligst på GitHub eller send det til {email}, og vedhæft logfilen.", + "crash_dialog_privacy": "Præcis dette bliver delt. Intet sendes automatisk — knapperne åbner kun en udfyldt GitHub-sag eller e-mail. Din hjemmemappe, dit brugernavn og tokens er fjernet.", "crash_dialog_github": "Rapportér på GitHub", - "crash_dialog_facebook": "Del i Facebook-gruppen", "crash_dialog_email": "Send via e-mail", "crash_dialog_copy": "Kopiér rapport", "crash_dialog_copied": "✓ Kopieret", diff --git a/src/opensak/lang/de.py b/src/opensak/lang/de.py index 3841248e..508619b3 100644 --- a/src/opensak/lang/de.py +++ b/src/opensak/lang/de.py @@ -1809,11 +1809,10 @@ "gsak_filter_import_failed_header": "{count} Filter konnten nicht umgewandelt werden:", "crash_dialog_title": "OpenSAK ist auf ein Problem gestoßen", - "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder in unserer Facebook-Gruppe oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder in unserer Facebook-Gruppe oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail bzw. kopieren den Bericht und öffnen die Facebook-Gruppe zum Einfügen. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", + "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", "crash_dialog_github": "Auf GitHub melden", - "crash_dialog_facebook": "In Facebook-Gruppe posten", "crash_dialog_email": "Per E-Mail senden", "crash_dialog_copy": "Bericht kopieren", "crash_dialog_copied": "✓ Kopiert", diff --git a/src/opensak/lang/de_CH.py b/src/opensak/lang/de_CH.py index e4f6416f..ff36770d 100644 --- a/src/opensak/lang/de_CH.py +++ b/src/opensak/lang/de_CH.py @@ -1810,11 +1810,10 @@ "gsak_filter_import_failed_header": "{count} Filter konnten nicht umgewandelt werden:", "crash_dialog_title": "OpenSAK ist auf ein Problem gestossen", - "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder in unserer Facebook-Gruppe oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder in unserer Facebook-Gruppe oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail bzw. kopieren den Bericht und öffnen die Facebook-Gruppe zum Einfügen. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", + "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", + "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", "crash_dialog_github": "Auf GitHub melden", - "crash_dialog_facebook": "In Facebook-Gruppe posten", "crash_dialog_email": "Per E-Mail senden", "crash_dialog_copy": "Bericht kopieren", "crash_dialog_copied": "✓ Kopiert", diff --git a/src/opensak/lang/en.py b/src/opensak/lang/en.py index 5ce22120..59aa7598 100644 --- a/src/opensak/lang/en.py +++ b/src/opensak/lang/en.py @@ -1807,11 +1807,10 @@ "gsak_filter_import_failed_header": "{count} filter(s) could not be converted:", "crash_dialog_title": "OpenSAK ran into a problem", - "crash_dialog_msg": "OpenSAK hit an unexpected error. You can try to continue, but it is safer to save your work and restart.\n\nPlease help us fix it: report it on GitHub or in our Facebook group, or send it to {email}, and attach the log file.", - "crash_dialog_previous_msg": "OpenSAK did not close normally the last time it ran — it may have crashed.\n\nIf you remember what you were doing, please report it on GitHub or in our Facebook group, or send it to {email}, and attach the log file.", - "crash_dialog_privacy": "This is exactly what will be shared. Nothing is sent automatically — the buttons only open a prefilled GitHub issue or e-mail, or copy the report and open the Facebook group to paste it. Your home folder, user name and tokens have been removed.", + "crash_dialog_msg": "OpenSAK hit an unexpected error. You can try to continue, but it is safer to save your work and restart.\n\nPlease help us fix it: report it on GitHub or send it to {email}, and attach the log file.", + "crash_dialog_previous_msg": "OpenSAK did not close normally the last time it ran — it may have crashed.\n\nIf you remember what you were doing, please report it on GitHub or send it to {email}, and attach the log file.", + "crash_dialog_privacy": "This is exactly what will be shared. Nothing is sent automatically — the buttons only open a prefilled GitHub issue or e-mail. Your home folder, user name and tokens have been removed.", "crash_dialog_github": "Report on GitHub", - "crash_dialog_facebook": "Post in Facebook group", "crash_dialog_email": "Send by e-mail", "crash_dialog_copy": "Copy report", "crash_dialog_copied": "✓ Copied", diff --git a/src/opensak/lang/es.py b/src/opensak/lang/es.py index 16696235..4e55a53c 100644 --- a/src/opensak/lang/es.py +++ b/src/opensak/lang/es.py @@ -1809,11 +1809,10 @@ "gsak_filter_import_failed_header": "No se pudieron convertir {count} filtro(s):", "crash_dialog_title": "OpenSAK ha encontrado un problema", - "crash_dialog_msg": "OpenSAK ha sufrido un error inesperado. Puedes intentar continuar, pero es más seguro guardar tu trabajo y reiniciar.\n\nAyúdanos a corregirlo: infórmalo en GitHub o en nuestro grupo de Facebook, o envíalo a {email}, y adjunta el archivo de registro.", - "crash_dialog_previous_msg": "OpenSAK no se cerró correctamente la última vez que se ejecutó; puede que se haya bloqueado.\n\nSi recuerdas lo que estabas haciendo, infórmalo en GitHub o en nuestro grupo de Facebook, o envíalo a {email}, y adjunta el archivo de registro.", - "crash_dialog_privacy": "Esto es exactamente lo que se compartirá. No se envía nada automáticamente: los botones solo abren una incidencia de GitHub o un correo ya rellenados, o copian el informe y abren el grupo de Facebook para pegarlo. Se han eliminado tu carpeta personal, tu nombre de usuario y los tokens.", + "crash_dialog_msg": "OpenSAK ha sufrido un error inesperado. Puedes intentar continuar, pero es más seguro guardar tu trabajo y reiniciar.\n\nAyúdanos a corregirlo: infórmalo en GitHub o envíalo a {email}, y adjunta el archivo de registro.", + "crash_dialog_previous_msg": "OpenSAK no se cerró correctamente la última vez que se ejecutó; puede que se haya bloqueado.\n\nSi recuerdas lo que estabas haciendo, infórmalo en GitHub o envíalo a {email}, y adjunta el archivo de registro.", + "crash_dialog_privacy": "Esto es exactamente lo que se compartirá. No se envía nada automáticamente: los botones solo abren una incidencia de GitHub o un correo ya rellenados. Se han eliminado tu carpeta personal, tu nombre de usuario y los tokens.", "crash_dialog_github": "Informar en GitHub", - "crash_dialog_facebook": "Publicar en el grupo de Facebook", "crash_dialog_email": "Enviar por correo", "crash_dialog_copy": "Copiar informe", "crash_dialog_copied": "✓ Copiado", diff --git a/src/opensak/lang/fr.py b/src/opensak/lang/fr.py index 022538e6..04625bc3 100644 --- a/src/opensak/lang/fr.py +++ b/src/opensak/lang/fr.py @@ -1809,11 +1809,10 @@ "gsak_filter_import_failed_header": "{count} filtre(s) n'ont pas pu être convertis :", "crash_dialog_title": "OpenSAK a rencontré un problème", - "crash_dialog_msg": "OpenSAK a rencontré une erreur inattendue. Vous pouvez essayer de continuer, mais il est plus sûr d'enregistrer votre travail et de redémarrer.\n\nAidez-nous à la corriger : signalez-la sur GitHub ou dans notre groupe Facebook, ou envoyez-la à {email}, en joignant le fichier journal.", - "crash_dialog_previous_msg": "OpenSAK ne s'est pas fermé normalement lors de sa dernière exécution — il a peut-être planté.\n\nSi vous vous souvenez de ce que vous faisiez, signalez-le sur GitHub ou dans notre groupe Facebook, ou envoyez-le à {email}, en joignant le fichier journal.", - "crash_dialog_privacy": "C'est exactement ce qui sera partagé. Rien n'est envoyé automatiquement — les boutons ouvrent seulement un ticket GitHub ou un e-mail pré-rempli, ou copient le rapport et ouvrent le groupe Facebook pour le coller. Votre dossier personnel, votre nom d'utilisateur et vos jetons ont été supprimés.", + "crash_dialog_msg": "OpenSAK a rencontré une erreur inattendue. Vous pouvez essayer de continuer, mais il est plus sûr d'enregistrer votre travail et de redémarrer.\n\nAidez-nous à la corriger : signalez-la sur GitHub ou envoyez-la à {email}, en joignant le fichier journal.", + "crash_dialog_previous_msg": "OpenSAK ne s'est pas fermé normalement lors de sa dernière exécution — il a peut-être planté.\n\nSi vous vous souvenez de ce que vous faisiez, signalez-le sur GitHub ou envoyez-le à {email}, en joignant le fichier journal.", + "crash_dialog_privacy": "C'est exactement ce qui sera partagé. Rien n'est envoyé automatiquement — les boutons ouvrent seulement un ticket GitHub ou un e-mail pré-rempli. Votre dossier personnel, votre nom d'utilisateur et vos jetons ont été supprimés.", "crash_dialog_github": "Signaler sur GitHub", - "crash_dialog_facebook": "Publier dans le groupe Facebook", "crash_dialog_email": "Envoyer par e-mail", "crash_dialog_copy": "Copier le rapport", "crash_dialog_copied": "✓ Copié", diff --git a/src/opensak/lang/nl.py b/src/opensak/lang/nl.py index 84f127c8..83a1a6b9 100644 --- a/src/opensak/lang/nl.py +++ b/src/opensak/lang/nl.py @@ -1806,11 +1806,10 @@ "gsak_filter_import_failed_header": "{count} filter(s) konden niet omgezet worden:", "crash_dialog_title": "OpenSAK is tegen een probleem aangelopen", - "crash_dialog_msg": "Er is een onverwachte fout opgetreden in OpenSAK. U kunt proberen door te gaan, maar het is veiliger om uw werk op te slaan en opnieuw te starten.\n\nHelp ons de fout op te lossen: meld deze op GitHub of in onze Facebook-groep, of stuur deze naar {email}, en voeg het logbestand toe.", - "crash_dialog_previous_msg": "OpenSAK is de vorige keer niet normaal afgesloten — mogelijk is het gecrasht.\n\nAls u nog weet wat u aan het doen was, meld het dan op GitHub of in onze Facebook-groep, of stuur het naar {email}, en voeg het logbestand toe.", - "crash_dialog_privacy": "Precies dit wordt gedeeld. Er wordt niets automatisch verzonden — de knoppen openen alleen een vooraf ingevuld GitHub-issue of e-mail, of kopiëren het rapport en openen de Facebook-groep om het te plakken. Uw thuismap, gebruikersnaam en tokens zijn verwijderd.", + "crash_dialog_msg": "Er is een onverwachte fout opgetreden in OpenSAK. U kunt proberen door te gaan, maar het is veiliger om uw werk op te slaan en opnieuw te starten.\n\nHelp ons de fout op te lossen: meld deze op GitHub of stuur deze naar {email}, en voeg het logbestand toe.", + "crash_dialog_previous_msg": "OpenSAK is de vorige keer niet normaal afgesloten — mogelijk is het gecrasht.\n\nAls u nog weet wat u aan het doen was, meld het dan op GitHub of stuur het naar {email}, en voeg het logbestand toe.", + "crash_dialog_privacy": "Precies dit wordt gedeeld. Er wordt niets automatisch verzonden — de knoppen openen alleen een vooraf ingevuld GitHub-issue of e-mail. Uw thuismap, gebruikersnaam en tokens zijn verwijderd.", "crash_dialog_github": "Melden op GitHub", - "crash_dialog_facebook": "Plaatsen in Facebook-groep", "crash_dialog_email": "Versturen per e-mail", "crash_dialog_copy": "Rapport kopiëren", "crash_dialog_copied": "✓ Gekopieerd", diff --git a/src/opensak/lang/pl.py b/src/opensak/lang/pl.py index b806953f..85fd1d11 100644 --- a/src/opensak/lang/pl.py +++ b/src/opensak/lang/pl.py @@ -1809,11 +1809,10 @@ "gsak_filter_import_failed_header": "Nie udało się przekonwertować {count} filtrów:", "crash_dialog_title": "OpenSAK napotkał problem", - "crash_dialog_msg": "W OpenSAK wystąpił nieoczekiwany błąd. Możesz spróbować kontynuować, ale bezpieczniej jest zapisać pracę i uruchomić program ponownie.\n\nPomóż nam go naprawić: zgłoś go na GitHubie lub w naszej grupie na Facebooku albo wyślij na {email} i dołącz plik dziennika.", - "crash_dialog_previous_msg": "OpenSAK nie został poprawnie zamknięty podczas ostatniego uruchomienia — mógł ulec awarii.\n\nJeśli pamiętasz, co robiłeś, zgłoś to na GitHubie lub w naszej grupie na Facebooku albo wyślij na {email} i dołącz plik dziennika.", - "crash_dialog_privacy": "Dokładnie to zostanie udostępnione. Nic nie jest wysyłane automatycznie — przyciski otwierają jedynie wstępnie wypełnione zgłoszenie na GitHubie lub e-mail albo kopiują raport i otwierają grupę na Facebooku, aby go wkleić. Twój folder domowy, nazwa użytkownika i tokeny zostały usunięte.", + "crash_dialog_msg": "W OpenSAK wystąpił nieoczekiwany błąd. Możesz spróbować kontynuować, ale bezpieczniej jest zapisać pracę i uruchomić program ponownie.\n\nPomóż nam go naprawić: zgłoś go na GitHubie lub wyślij na {email} i dołącz plik dziennika.", + "crash_dialog_previous_msg": "OpenSAK nie został poprawnie zamknięty podczas ostatniego uruchomienia — mógł ulec awarii.\n\nJeśli pamiętasz, co robiłeś, zgłoś to na GitHubie lub wyślij na {email} i dołącz plik dziennika.", + "crash_dialog_privacy": "Dokładnie to zostanie udostępnione. Nic nie jest wysyłane automatycznie — przyciski otwierają jedynie wstępnie wypełnione zgłoszenie na GitHubie lub e-mail. Twój folder domowy, nazwa użytkownika i tokeny zostały usunięte.", "crash_dialog_github": "Zgłoś na GitHubie", - "crash_dialog_facebook": "Opublikuj w grupie na Facebooku", "crash_dialog_email": "Wyślij e-mailem", "crash_dialog_copy": "Kopiuj raport", "crash_dialog_copied": "✓ Skopiowano", diff --git a/src/opensak/lang/pt.py b/src/opensak/lang/pt.py index 52001c85..a16822e3 100644 --- a/src/opensak/lang/pt.py +++ b/src/opensak/lang/pt.py @@ -1809,11 +1809,10 @@ "gsak_filter_import_failed_header": "{count} filtro(s) não puderam ser convertidos:", "crash_dialog_title": "O OpenSAK encontrou um problema", - "crash_dialog_msg": "O OpenSAK encontrou um erro inesperado. Pode tentar continuar, mas é mais seguro guardar o seu trabalho e reiniciar.\n\nAjude-nos a corrigi-lo: reporte-o no GitHub ou no nosso grupo do Facebook, ou envie-o para {email}, e anexe o ficheiro de registo.", - "crash_dialog_previous_msg": "O OpenSAK não foi fechado normalmente da última vez que foi executado — pode ter falhado.\n\nSe se lembrar do que estava a fazer, reporte-o no GitHub ou no nosso grupo do Facebook, ou envie-o para {email}, e anexe o ficheiro de registo.", - "crash_dialog_privacy": "É exatamente isto que será partilhado. Nada é enviado automaticamente — os botões apenas abrem um issue do GitHub ou um e-mail pré-preenchido, ou copiam o relatório e abrem o grupo do Facebook para o colar. A sua pasta pessoal, nome de utilizador e tokens foram removidos.", + "crash_dialog_msg": "O OpenSAK encontrou um erro inesperado. Pode tentar continuar, mas é mais seguro guardar o seu trabalho e reiniciar.\n\nAjude-nos a corrigi-lo: reporte-o no GitHub ou envie-o para {email}, e anexe o ficheiro de registo.", + "crash_dialog_previous_msg": "O OpenSAK não foi fechado normalmente da última vez que foi executado — pode ter falhado.\n\nSe se lembrar do que estava a fazer, reporte-o no GitHub ou envie-o para {email}, e anexe o ficheiro de registo.", + "crash_dialog_privacy": "É exatamente isto que será partilhado. Nada é enviado automaticamente — os botões apenas abrem um issue do GitHub ou um e-mail pré-preenchido. A sua pasta pessoal, nome de utilizador e tokens foram removidos.", "crash_dialog_github": "Reportar no GitHub", - "crash_dialog_facebook": "Publicar no grupo do Facebook", "crash_dialog_email": "Enviar por e-mail", "crash_dialog_copy": "Copiar relatório", "crash_dialog_copied": "✓ Copiado", diff --git a/src/opensak/lang/se.py b/src/opensak/lang/se.py index 3257cd2e..823f213a 100644 --- a/src/opensak/lang/se.py +++ b/src/opensak/lang/se.py @@ -1808,11 +1808,10 @@ "gsak_filter_import_failed_header": "{count} filter kunde inte konverteras:", "crash_dialog_title": "OpenSAK stötte på ett problem", - "crash_dialog_msg": "Ett oväntat fel uppstod i OpenSAK. Du kan försöka fortsätta, men det är säkrare att spara ditt arbete och starta om.\n\nHjälp oss att rätta det: rapportera det på GitHub eller i vår Facebook-grupp, eller skicka det till {email}, och bifoga loggfilen.", - "crash_dialog_previous_msg": "OpenSAK stängdes inte normalt senast det kördes — det kan ha kraschat.\n\nOm du minns vad du gjorde, rapportera det på GitHub eller i vår Facebook-grupp, eller skicka det till {email}, och bifoga loggfilen.", - "crash_dialog_privacy": "Exakt detta kommer att delas. Inget skickas automatiskt — knapparna öppnar bara ett förifyllt GitHub-ärende eller e-postmeddelande, eller kopierar rapporten och öppnar Facebook-gruppen så att du kan klistra in den. Din hemmapp, ditt användarnamn och tokens har tagits bort.", + "crash_dialog_msg": "Ett oväntat fel uppstod i OpenSAK. Du kan försöka fortsätta, men det är säkrare att spara ditt arbete och starta om.\n\nHjälp oss att rätta det: rapportera det på GitHub eller skicka det till {email}, och bifoga loggfilen.", + "crash_dialog_previous_msg": "OpenSAK stängdes inte normalt senast det kördes — det kan ha kraschat.\n\nOm du minns vad du gjorde, rapportera det på GitHub eller skicka det till {email}, och bifoga loggfilen.", + "crash_dialog_privacy": "Exakt detta kommer att delas. Inget skickas automatiskt — knapparna öppnar bara ett förifyllt GitHub-ärende eller e-postmeddelande. Din hemmapp, ditt användarnamn och tokens har tagits bort.", "crash_dialog_github": "Rapportera på GitHub", - "crash_dialog_facebook": "Posta i Facebook-gruppen", "crash_dialog_email": "Skicka via e-post", "crash_dialog_copy": "Kopiera rapport", "crash_dialog_copied": "✓ Kopierat", diff --git a/tests/unit-tests/test_crash_report.py b/tests/unit-tests/test_crash_report.py index fcd2da4a..2f976fd7 100644 --- a/tests/unit-tests/test_crash_report.py +++ b/tests/unit-tests/test_crash_report.py @@ -312,13 +312,11 @@ def test_github_opens_issue_and_log_folder(self, qtbot, opened): assert opened[0].startswith(cr.GITHUB_NEW_ISSUE_URL) assert opened[1].startswith("file:") - def test_facebook_copies_report_and_opens_group(self, qtbot, opened): - from PySide6.QtWidgets import QApplication + def test_report_channels_are_primary_helpers_secondary(self, qtbot): dlg = self._make(qtbot) - dlg._btn_facebook.click() - assert QApplication.clipboard().text() == "Traceback\nValueError: x" - assert opened[0] == cr.FACEBOOK_GROUP_URL - assert opened[1].startswith("file:") + assert not dlg._btn_github.isFlat() and not dlg._btn_email.isFlat() + assert dlg._btn_copy.isFlat() and dlg._btn_log.isFlat() + assert not hasattr(dlg, "_btn_facebook") def test_email_opens_mailto(self, qtbot, opened): dlg = self._make(qtbot) From 239e4d0e55804b7d945592479974394783de7400 Mon Sep 17 00:00:00 2001 From: nagisml Date: Sun, 18 Oct 2026 14:19:58 +0200 Subject: [PATCH 8/8] Update policies and documentation texts --- PRIVACY_POLICY.md | 17 +++++++++++++++-- docs/Uninstalling-OpenSAK.md | 5 +++++ site/PRIVACY_POLICY.html | 8 ++++++-- src/opensak/lang/cs.py | 2 +- src/opensak/lang/da.py | 2 +- src/opensak/lang/de.py | 2 +- src/opensak/lang/de_CH.py | 2 +- src/opensak/lang/en.py | 2 +- src/opensak/lang/es.py | 2 +- src/opensak/lang/fr.py | 2 +- src/opensak/lang/nl.py | 2 +- src/opensak/lang/pl.py | 2 +- src/opensak/lang/pt.py | 2 +- src/opensak/lang/se.py | 2 +- 14 files changed, 37 insertions(+), 15 deletions(-) diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md index ad92f080..fc8edd15 100644 --- a/PRIVACY_POLICY.md +++ b/PRIVACY_POLICY.md @@ -1,7 +1,7 @@ # Privacy Policy — OpenSAK **Open Source Geocache Manager** -*Effective date: September 29, 2026* +*Effective date: October 18, 2026* --- @@ -69,6 +69,19 @@ If you set up **Pocket Queries by e-mail**, OpenSAK stores your mail server name When you check for PQ e-mails, OpenSAK connects **only to the mail server you entered**, over an encrypted connection, and verifies the server's certificate before sending your password. It reads messages to find Pocket Query attachments and, if you chose so, deletes the e-mail after a successful import. Nothing from your mailbox is sent anywhere else. +### 2.5 Logs and crash reports + +OpenSAK writes a log file (`opensak.log`, plus `opensak.log.previous` from the session before) to its application data directory. To notice crashes, it also keeps a `session.lock` file while it runs and, if it crashes hard, writes the technical details to `crash_native.log` in the same place. None of these files leave your device on their own. + +When OpenSAK runs into an unexpected error, or finds at startup that the previous session crashed, it shows a crash report dialog with the **exact text** that would be shared. Your home folder path, your computer's user name, e-mail addresses and access tokens are removed from it first. Other details, such as cache or database names, may still appear, so you can check the text before sharing it. + +A crash report is shared **only if you choose to**: + +- **Report on GitHub** opens a prefilled issue in your browser, which you can still edit and must submit yourself. GitHub's privacy statement applies. +- **Send e-mail** opens a prefilled message to hello@opensak.com in your own mail program, which you send yourself. + +Nothing is sent automatically, and you can simply close the dialog. + --- ## 3. Data We Do Not Collect @@ -76,7 +89,7 @@ When you check for PQ e-mails, OpenSAK connects **only to the mail server you en OpenSAK does not collect, transmit, or process any of the following: - Usage statistics or analytics -- Crash reports or error telemetry +- Crash reports or error telemetry (a crash report is only shared when you send it yourself, see 2.5) - Your location or GPS data - Your name, email address, or Geocaching.com username (except as stored in locally imported cache logs) - Any data about how you use the application diff --git a/docs/Uninstalling-OpenSAK.md b/docs/Uninstalling-OpenSAK.md index 8522754d..fca8a79c 100644 --- a/docs/Uninstalling-OpenSAK.md +++ b/docs/Uninstalling-OpenSAK.md @@ -67,6 +67,11 @@ changed in version 1.14.0: | Language only | `preferences.json` | merged into `opensak.json` | | Database(s) | Same folder as settings | A folder you chose in the welcome wizard (can be the same folder, or a different one) | | Logs | `opensak.log` (same folder) | `opensak.log` inside the install folder | +| Crash detection | — | `session.lock` and `crash_native.log` inside the install folder | + +`session.lock` exists while OpenSAK is running (and stays behind if it was +closed abnormally); `crash_native.log` holds the technical details of the last +hard crash, if any. Both are safe to delete while OpenSAK is closed. If you upgraded from an older version to 1.14.0+, OpenSAK automatically copied your old settings into the new `opensak.json` the first time it ran. The old diff --git a/site/PRIVACY_POLICY.html b/site/PRIVACY_POLICY.html index 234cfa41..038a02c7 100644 --- a/site/PRIVACY_POLICY.html +++ b/site/PRIVACY_POLICY.html @@ -50,7 +50,7 @@

// Legal

Privacy Policy

-

Effective date: September 29, 2026

+

Effective date: October 18, 2026

1. Overview

OpenSAK is a free, open source desktop application for managing geocaching data locally on your own computer. It runs on Windows, Linux, and macOS.

@@ -89,12 +89,16 @@

2.4 Pocket Query e-mail settings (optional)

If you set up Pocket Queries by e-mail, your mail server, port, security setting and username are stored in OpenSAK's settings file. The password is stored in your operating system's secure credential store (macOS Keychain, Windows Credential Manager, or the Secret Service keyring on Linux) — never in plain text.

OpenSAK connects only to the mail server you entered, over an encrypted connection, and verifies the server's certificate before sending your password. Nothing from your mailbox is sent anywhere else.

+

2.5 Logs and crash reports

+

OpenSAK keeps a log file (opensak.log and opensak.log.previous) in its application data directory. To notice crashes it also keeps session.lock while it runs and, after a hard crash, crash_native.log in the same place. None of these files leave your device on their own.

+

After an unexpected error or a crash, OpenSAK shows a crash report dialog with the exact text that would be shared, with your home folder path, your computer's user name, e-mail addresses and access tokens removed. Other details, such as cache or database names, may still appear. The report is shared only if you choose to: Report on GitHub opens a prefilled issue that you submit yourself (GitHub's privacy statement applies), and Send e-mail opens a prefilled message to hello@opensak.com in your own mail program. Nothing is sent automatically.

+

3. Data We Do Not Collect

  • Usage statistics or analytics
  • -
  • Crash reports or error telemetry
  • +
  • Crash reports or error telemetry (a crash report is only shared when you send it yourself, see 2.5)
  • Your location or GPS data
  • Your name or email address
  • Any data about how you use the application
  • diff --git a/src/opensak/lang/cs.py b/src/opensak/lang/cs.py index 951960ae..2f3c135e 100644 --- a/src/opensak/lang/cs.py +++ b/src/opensak/lang/cs.py @@ -1806,7 +1806,7 @@ "crash_dialog_title": "OpenSAK narazil na problém", "crash_dialog_msg": "V OpenSAK došlo k neočekávané chybě. Můžete zkusit pokračovat, ale bezpečnější je uložit práci a restartovat.\n\nPomozte nám ji opravit: nahlaste ji na GitHubu nebo ji pošlete na {email} a přiložte soubor protokolu.", "crash_dialog_previous_msg": "OpenSAK se při posledním spuštění neukončil normálně — mohl spadnout.\n\nPokud si pamatujete, co jste dělali, nahlaste to prosím na GitHubu nebo to pošlete na {email} a přiložte soubor protokolu.", - "crash_dialog_privacy": "Přesně toto bude sdíleno. Nic se neodesílá automaticky — tlačítka pouze otevřou předvyplněné hlášení na GitHubu nebo e-mail. Vaše domovská složka, uživatelské jméno a tokeny byly odstraněny.", + "crash_dialog_privacy": "Přesně toto bude sdíleno. Nic se neodesílá automaticky — tlačítka pouze otevřou předvyplněné hlášení na GitHubu nebo e-mail. Vaše domovská složka, uživatelské jméno v počítači a tokeny byly odstraněny.", "crash_dialog_github": "Nahlásit na GitHubu", "crash_dialog_email": "Odeslat e-mailem", "crash_dialog_copy": "Kopírovat hlášení", diff --git a/src/opensak/lang/da.py b/src/opensak/lang/da.py index 3e838ff4..cfb45949 100644 --- a/src/opensak/lang/da.py +++ b/src/opensak/lang/da.py @@ -1810,7 +1810,7 @@ "crash_dialog_title": "OpenSAK stødte på et problem", "crash_dialog_msg": "OpenSAK ramte en uventet fejl. Du kan prøve at fortsætte, men det er sikrest at gemme dit arbejde og genstarte.\n\nHjælp os med at rette den: rapportér den på GitHub eller send den til {email}, og vedhæft logfilen.", "crash_dialog_previous_msg": "OpenSAK blev ikke lukket normalt sidst den kørte — den kan være gået ned.\n\nHvis du husker, hvad du lavede, så rapportér det venligst på GitHub eller send det til {email}, og vedhæft logfilen.", - "crash_dialog_privacy": "Præcis dette bliver delt. Intet sendes automatisk — knapperne åbner kun en udfyldt GitHub-sag eller e-mail. Din hjemmemappe, dit brugernavn og tokens er fjernet.", + "crash_dialog_privacy": "Præcis dette bliver delt. Intet sendes automatisk — knapperne åbner kun en udfyldt GitHub-sag eller e-mail. Din hjemmemappe, din computers brugernavn og tokens er fjernet.", "crash_dialog_github": "Rapportér på GitHub", "crash_dialog_email": "Send via e-mail", "crash_dialog_copy": "Kopiér rapport", diff --git a/src/opensak/lang/de.py b/src/opensak/lang/de.py index 508619b3..c4f83ad4 100644 --- a/src/opensak/lang/de.py +++ b/src/opensak/lang/de.py @@ -1811,7 +1811,7 @@ "crash_dialog_title": "OpenSAK ist auf ein Problem gestoßen", "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", + "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, der Benutzername Ihres Computers und Tokens wurden entfernt.", "crash_dialog_github": "Auf GitHub melden", "crash_dialog_email": "Per E-Mail senden", "crash_dialog_copy": "Bericht kopieren", diff --git a/src/opensak/lang/de_CH.py b/src/opensak/lang/de_CH.py index ff36770d..69613fd0 100644 --- a/src/opensak/lang/de_CH.py +++ b/src/opensak/lang/de_CH.py @@ -1812,7 +1812,7 @@ "crash_dialog_title": "OpenSAK ist auf ein Problem gestossen", "crash_dialog_msg": "In OpenSAK ist ein unerwarteter Fehler aufgetreten. Sie können versuchen fortzufahren, sicherer ist es aber, Ihre Arbeit zu speichern und neu zu starten.\n\nBitte helfen Sie uns, ihn zu beheben: Melden Sie ihn auf GitHub oder senden Sie ihn an {email} und hängen Sie die Protokolldatei an.", "crash_dialog_previous_msg": "OpenSAK wurde beim letzten Mal nicht normal beendet — möglicherweise ist es abgestürzt.\n\nWenn Sie sich erinnern, was Sie gerade gemacht haben, melden Sie es bitte auf GitHub oder senden Sie es an {email} und hängen Sie die Protokolldatei an.", - "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, Benutzername und Tokens wurden entfernt.", + "crash_dialog_privacy": "Genau das wird geteilt. Nichts wird automatisch gesendet — die Schaltflächen öffnen nur ein vorausgefülltes GitHub-Issue oder eine E-Mail. Ihr Benutzerordner, der Benutzername Ihres Computers und Tokens wurden entfernt.", "crash_dialog_github": "Auf GitHub melden", "crash_dialog_email": "Per E-Mail senden", "crash_dialog_copy": "Bericht kopieren", diff --git a/src/opensak/lang/en.py b/src/opensak/lang/en.py index 59aa7598..9b5dae00 100644 --- a/src/opensak/lang/en.py +++ b/src/opensak/lang/en.py @@ -1809,7 +1809,7 @@ "crash_dialog_title": "OpenSAK ran into a problem", "crash_dialog_msg": "OpenSAK hit an unexpected error. You can try to continue, but it is safer to save your work and restart.\n\nPlease help us fix it: report it on GitHub or send it to {email}, and attach the log file.", "crash_dialog_previous_msg": "OpenSAK did not close normally the last time it ran — it may have crashed.\n\nIf you remember what you were doing, please report it on GitHub or send it to {email}, and attach the log file.", - "crash_dialog_privacy": "This is exactly what will be shared. Nothing is sent automatically — the buttons only open a prefilled GitHub issue or e-mail. Your home folder, user name and tokens have been removed.", + "crash_dialog_privacy": "This is exactly what will be shared. Nothing is sent automatically — the buttons only open a prefilled GitHub issue or e-mail. Your home folder, your computer's user name and tokens have been removed.", "crash_dialog_github": "Report on GitHub", "crash_dialog_email": "Send by e-mail", "crash_dialog_copy": "Copy report", diff --git a/src/opensak/lang/es.py b/src/opensak/lang/es.py index 4e55a53c..81cccc2a 100644 --- a/src/opensak/lang/es.py +++ b/src/opensak/lang/es.py @@ -1811,7 +1811,7 @@ "crash_dialog_title": "OpenSAK ha encontrado un problema", "crash_dialog_msg": "OpenSAK ha sufrido un error inesperado. Puedes intentar continuar, pero es más seguro guardar tu trabajo y reiniciar.\n\nAyúdanos a corregirlo: infórmalo en GitHub o envíalo a {email}, y adjunta el archivo de registro.", "crash_dialog_previous_msg": "OpenSAK no se cerró correctamente la última vez que se ejecutó; puede que se haya bloqueado.\n\nSi recuerdas lo que estabas haciendo, infórmalo en GitHub o envíalo a {email}, y adjunta el archivo de registro.", - "crash_dialog_privacy": "Esto es exactamente lo que se compartirá. No se envía nada automáticamente: los botones solo abren una incidencia de GitHub o un correo ya rellenados. Se han eliminado tu carpeta personal, tu nombre de usuario y los tokens.", + "crash_dialog_privacy": "Esto es exactamente lo que se compartirá. No se envía nada automáticamente: los botones solo abren una incidencia de GitHub o un correo ya rellenados. Se han eliminado tu carpeta personal, el nombre de usuario de tu ordenador y los tokens.", "crash_dialog_github": "Informar en GitHub", "crash_dialog_email": "Enviar por correo", "crash_dialog_copy": "Copiar informe", diff --git a/src/opensak/lang/fr.py b/src/opensak/lang/fr.py index 04625bc3..b07f8584 100644 --- a/src/opensak/lang/fr.py +++ b/src/opensak/lang/fr.py @@ -1811,7 +1811,7 @@ "crash_dialog_title": "OpenSAK a rencontré un problème", "crash_dialog_msg": "OpenSAK a rencontré une erreur inattendue. Vous pouvez essayer de continuer, mais il est plus sûr d'enregistrer votre travail et de redémarrer.\n\nAidez-nous à la corriger : signalez-la sur GitHub ou envoyez-la à {email}, en joignant le fichier journal.", "crash_dialog_previous_msg": "OpenSAK ne s'est pas fermé normalement lors de sa dernière exécution — il a peut-être planté.\n\nSi vous vous souvenez de ce que vous faisiez, signalez-le sur GitHub ou envoyez-le à {email}, en joignant le fichier journal.", - "crash_dialog_privacy": "C'est exactement ce qui sera partagé. Rien n'est envoyé automatiquement — les boutons ouvrent seulement un ticket GitHub ou un e-mail pré-rempli. Votre dossier personnel, votre nom d'utilisateur et vos jetons ont été supprimés.", + "crash_dialog_privacy": "C'est exactement ce qui sera partagé. Rien n'est envoyé automatiquement — les boutons ouvrent seulement un ticket GitHub ou un e-mail pré-rempli. Votre dossier personnel, le nom d'utilisateur de votre ordinateur et vos jetons ont été supprimés.", "crash_dialog_github": "Signaler sur GitHub", "crash_dialog_email": "Envoyer par e-mail", "crash_dialog_copy": "Copier le rapport", diff --git a/src/opensak/lang/nl.py b/src/opensak/lang/nl.py index 83a1a6b9..1ac96814 100644 --- a/src/opensak/lang/nl.py +++ b/src/opensak/lang/nl.py @@ -1808,7 +1808,7 @@ "crash_dialog_title": "OpenSAK is tegen een probleem aangelopen", "crash_dialog_msg": "Er is een onverwachte fout opgetreden in OpenSAK. U kunt proberen door te gaan, maar het is veiliger om uw werk op te slaan en opnieuw te starten.\n\nHelp ons de fout op te lossen: meld deze op GitHub of stuur deze naar {email}, en voeg het logbestand toe.", "crash_dialog_previous_msg": "OpenSAK is de vorige keer niet normaal afgesloten — mogelijk is het gecrasht.\n\nAls u nog weet wat u aan het doen was, meld het dan op GitHub of stuur het naar {email}, en voeg het logbestand toe.", - "crash_dialog_privacy": "Precies dit wordt gedeeld. Er wordt niets automatisch verzonden — de knoppen openen alleen een vooraf ingevuld GitHub-issue of e-mail. Uw thuismap, gebruikersnaam en tokens zijn verwijderd.", + "crash_dialog_privacy": "Precies dit wordt gedeeld. Er wordt niets automatisch verzonden — de knoppen openen alleen een vooraf ingevuld GitHub-issue of e-mail. Uw thuismap, de gebruikersnaam van uw computer en tokens zijn verwijderd.", "crash_dialog_github": "Melden op GitHub", "crash_dialog_email": "Versturen per e-mail", "crash_dialog_copy": "Rapport kopiëren", diff --git a/src/opensak/lang/pl.py b/src/opensak/lang/pl.py index 85fd1d11..803a2de7 100644 --- a/src/opensak/lang/pl.py +++ b/src/opensak/lang/pl.py @@ -1811,7 +1811,7 @@ "crash_dialog_title": "OpenSAK napotkał problem", "crash_dialog_msg": "W OpenSAK wystąpił nieoczekiwany błąd. Możesz spróbować kontynuować, ale bezpieczniej jest zapisać pracę i uruchomić program ponownie.\n\nPomóż nam go naprawić: zgłoś go na GitHubie lub wyślij na {email} i dołącz plik dziennika.", "crash_dialog_previous_msg": "OpenSAK nie został poprawnie zamknięty podczas ostatniego uruchomienia — mógł ulec awarii.\n\nJeśli pamiętasz, co robiłeś, zgłoś to na GitHubie lub wyślij na {email} i dołącz plik dziennika.", - "crash_dialog_privacy": "Dokładnie to zostanie udostępnione. Nic nie jest wysyłane automatycznie — przyciski otwierają jedynie wstępnie wypełnione zgłoszenie na GitHubie lub e-mail. Twój folder domowy, nazwa użytkownika i tokeny zostały usunięte.", + "crash_dialog_privacy": "Dokładnie to zostanie udostępnione. Nic nie jest wysyłane automatycznie — przyciski otwierają jedynie wstępnie wypełnione zgłoszenie na GitHubie lub e-mail. Twój folder domowy, nazwa użytkownika komputera i tokeny zostały usunięte.", "crash_dialog_github": "Zgłoś na GitHubie", "crash_dialog_email": "Wyślij e-mailem", "crash_dialog_copy": "Kopiuj raport", diff --git a/src/opensak/lang/pt.py b/src/opensak/lang/pt.py index a16822e3..dd300a0f 100644 --- a/src/opensak/lang/pt.py +++ b/src/opensak/lang/pt.py @@ -1811,7 +1811,7 @@ "crash_dialog_title": "O OpenSAK encontrou um problema", "crash_dialog_msg": "O OpenSAK encontrou um erro inesperado. Pode tentar continuar, mas é mais seguro guardar o seu trabalho e reiniciar.\n\nAjude-nos a corrigi-lo: reporte-o no GitHub ou envie-o para {email}, e anexe o ficheiro de registo.", "crash_dialog_previous_msg": "O OpenSAK não foi fechado normalmente da última vez que foi executado — pode ter falhado.\n\nSe se lembrar do que estava a fazer, reporte-o no GitHub ou envie-o para {email}, e anexe o ficheiro de registo.", - "crash_dialog_privacy": "É exatamente isto que será partilhado. Nada é enviado automaticamente — os botões apenas abrem um issue do GitHub ou um e-mail pré-preenchido. A sua pasta pessoal, nome de utilizador e tokens foram removidos.", + "crash_dialog_privacy": "É exatamente isto que será partilhado. Nada é enviado automaticamente — os botões apenas abrem um issue do GitHub ou um e-mail pré-preenchido. A sua pasta pessoal, o nome de utilizador do computador e os tokens foram removidos.", "crash_dialog_github": "Reportar no GitHub", "crash_dialog_email": "Enviar por e-mail", "crash_dialog_copy": "Copiar relatório", diff --git a/src/opensak/lang/se.py b/src/opensak/lang/se.py index 823f213a..e4f2f122 100644 --- a/src/opensak/lang/se.py +++ b/src/opensak/lang/se.py @@ -1810,7 +1810,7 @@ "crash_dialog_title": "OpenSAK stötte på ett problem", "crash_dialog_msg": "Ett oväntat fel uppstod i OpenSAK. Du kan försöka fortsätta, men det är säkrare att spara ditt arbete och starta om.\n\nHjälp oss att rätta det: rapportera det på GitHub eller skicka det till {email}, och bifoga loggfilen.", "crash_dialog_previous_msg": "OpenSAK stängdes inte normalt senast det kördes — det kan ha kraschat.\n\nOm du minns vad du gjorde, rapportera det på GitHub eller skicka det till {email}, och bifoga loggfilen.", - "crash_dialog_privacy": "Exakt detta kommer att delas. Inget skickas automatiskt — knapparna öppnar bara ett förifyllt GitHub-ärende eller e-postmeddelande. Din hemmapp, ditt användarnamn och tokens har tagits bort.", + "crash_dialog_privacy": "Exakt detta kommer att delas. Inget skickas automatiskt — knapparna öppnar bara ett förifyllt GitHub-ärende eller e-postmeddelande. Din hemmapp, datorns användarnamn och tokens har tagits bort.", "crash_dialog_github": "Rapportera på GitHub", "crash_dialog_email": "Skicka via e-post", "crash_dialog_copy": "Kopiera rapport",