Make Stop stop: no replay, no restart, immediate feedback
Build — Windows, Linux & macOS / GDPRScanner / linux (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / macos (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / windows (push) Has been cancelled
Build — Windows, Linux & macOS / Create GitHub Release (push) Has been cancelled

Pressing Stop could look like the scan restarting, and for scheduled jobs
it really did restart the Google stage.

- Scheduler: check the abort flag between stages and never clear the
  Google abort after a Stop (that started a fresh all-users Google scan).
  A stopped job is recorded as "cancelled" and skips email/retention;
  scheduler_done carries cancelled=true.
- SSE: scan_cancelled and file_scan_done now end the current scan like
  scan_done, and sse.replay_messages() never replays a finished scan.
  Previously a stopped M365 scan (and every file scan) stayed "current",
  so each reconnect replayed it and re-showed Stop + progress.
- Frontend: scan_cancelled clears the M365 running flag and only tears
  down the UI when no Google/file scan is still running; scheduler_done /
  scheduler_error clear all running flags.
- run_scan: Stop during collection now ends the scan (checkpoint, finish,
  scan_cancelled) instead of announcing scan_start / "Scanning…".
- Stop button: switches to a disabled "Stopping…" state on click; all
  show/hide goes through _setStopBtnDisplay(), which resets it when hidden
  or when a new scan starts.

Adds tests/test_stop_scan.py (10 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #2.
This commit is contained in:
StyxX65andClaude Opus 5.5 committed 2026-09-28 10:03:12 +02:00
1 parent 9cf23da907
commit 606d8e944b
12 files changed
+372 -64

No files matched your search

+27 -12
View File
@@ -351,6 +351,9 @@ class ScanScheduler:
)
_fs_extra = {k: _profile_opts[k] for k in _FS_OPT_KEYS if k in _profile_opts}
for fs in options.get("file_sources", []):
# Stop pressed during an earlier stage — don't start more stages
if _m._scan_abort.is_set():
break
# Resolve string IDs to full source dicts
if isinstance(fs, str):
fs = _all_file_sources.get(fs, {"path": fs, "label": fs})
@@ -374,7 +377,12 @@ class ScanScheduler:
# Legacy profiles store everything in sources[]
google_sources = [s for s in options.get("sources", [])
if s in ("gmail", "gdrive")]
if google_sources and state.google_connector:
# POST /api/scan/stop sets both abort events. _scan_abort was cleared
# when this job started, so if it is set now the user pressed Stop
# during an earlier stage — the Google stage must not start, and its
# abort event must not be cleared (that turned Stop into a restart).
stopped = _m._scan_abort.is_set()
if google_sources and state.google_connector and not stopped:
from routes.google_scan import (
_run_google_scan as _rgs,
_scan_lock as _gsl,
@@ -383,12 +391,15 @@ class ScanScheduler:
if _gsl.acquire(blocking=False):
try:
_gsa.clear()
logger.info("[scheduler] Starting Google scan — sources=%s", google_sources)
_rgs({
"sources": google_sources,
"user_emails": [], # empty → scan all workspace users
"options": options.get("options", {}),
})
# Stop sets _scan_abort first, so re-check after clearing:
# a Stop landing in between must still win.
if not _m._scan_abort.is_set():
logger.info("[scheduler] Starting Google scan — sources=%s", google_sources)
_rgs({
"sources": google_sources,
"user_emails": [], # empty → scan all workspace users
"options": options.get("options", {}),
})
except Exception as _ge:
import traceback as _tb3
logger.error("[scheduler] Google scan error: %s\n%s", _ge, _tb3.format_exc())
@@ -399,18 +410,21 @@ class ScanScheduler:
run["flagged"] = len(_m.flagged_items)
run["scanned"] = _m.scan_meta.get("total_scanned", 0)
run["status"] = "completed"
logger.info("[scheduler] Job '%s' completed — %d flagged, %d scanned",
job_cfg.get("name", ""), run["flagged"], run["scanned"])
stopped = _m._scan_abort.is_set()
run["status"] = "cancelled" if stopped else "completed"
logger.info("[scheduler] Job '%s' %s — %d flagged, %d scanned",
job_cfg.get("name", ""), run["status"], run["flagged"], run["scanned"])
if job_cfg.get("auto_email") and state.flagged_items:
# A stopped job's results are partial — don't email them or apply
# retention based on them.
if not stopped and job_cfg.get("auto_email") and state.flagged_items:
try:
self._send_email_report(job_cfg)
run["emailed"] = 1
except Exception as e:
run["error"] = f"Scan OK, email failed: {e}"
if job_cfg.get("auto_retention") and job_cfg.get("retention_years"):
if not stopped and job_cfg.get("auto_retention") and job_cfg.get("retention_years"):
try:
self._run_retention(job_cfg)
except Exception as e:
@@ -420,6 +434,7 @@ class ScanScheduler:
_m.broadcast("scheduler_done", {
"flagged": run["flagged"], "scanned": run["scanned"],
"emailed": run["emailed"], "job_name": job_cfg.get("name", ""),
"cancelled": stopped,
})
except Exception as e: