Files
GDPRScanner/sse.py
T
StyxX65andClaude Opus 5.5 606d8e944b
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
Make Stop stop: no replay, no restart, immediate feedback
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>
2026-09-28 10:03:12 +02:00

79 lines
3.4 KiB
Python

"""
sse.py — Server-Sent Events for GDPRScanner.
Provides:
broadcast(event, data) — push an event to all connected browsers
_sse_queues — list of per-connection Queue objects
_sse_buffer — deque replay buffer for late-connecting browsers
_current_scan_id — injected into every broadcast message
"""
from __future__ import annotations
import json
import logging
import queue
from collections import deque
logger = logging.getLogger(__name__)
# ── SSE state ─────────────────────────────────────────────────────────────────
_sse_queues: list = []
_sse_buffer: deque = deque(maxlen=500)
_current_scan_id: str = ""
# Scans that have ended (done / stopped). Their events stay in the buffer but
# are never replayed: replaying a stopped scan re-showed the Stop button and
# progress bar on every reconnect, so Stop looked like a restart.
_finished_scan_ids: deque = deque(maxlen=20)
# Events that end the scan identified by _current_scan_id.
# scan_done: M365 finished; scan_cancelled: M365 stopped; file_scan_done:
# file scan finished or stopped. (Google scans don't set _current_scan_id.)
_TERMINAL_EVENTS = frozenset({"scan_done", "scan_cancelled", "file_scan_done"})
def replay_messages() -> list[str]:
"""Buffered messages to replay to a newly connected browser.
While a scan runs, only that scan's events. Otherwise everything except
events of scans that have already ended (done or stopped).
"""
buf = list(_sse_buffer)
if _current_scan_id:
tag = f'"scan_id": "{_current_scan_id}"'
return [m for m in buf if tag in m]
finished = [f'"scan_id": "{sid}"' for sid in _finished_scan_ids]
return [m for m in buf if not any(tag in m for tag in finished)]
def broadcast(event: str, data: dict):
global _current_scan_id
if _current_scan_id:
data = {**data, "scan_id": _current_scan_id}
msg = f"event: {event}\ndata: {json.dumps(data)}\n\n"
_sse_buffer.append(msg) # buffer for SSE replay on reconnect
for q in list(_sse_queues):
try:
q.put_nowait(msg)
except queue.Full:
pass
# Clear scan_id when the scan ends so replay knows it is finished
if event in _TERMINAL_EVENTS and _current_scan_id:
_finished_scan_ids.append(_current_scan_id)
_current_scan_id = ""
# When no browser is watching (e.g. scheduled scan), log key events
if not _sse_queues:
if event == "scan_phase":
logger.info("[scan] %s", data.get("phase", ""))
elif event == "scan_progress":
file = data.get("file") or data.get("name", "")
if file:
logger.info("[scan] %s/%s — %s", data.get("completed", ""), data.get("total", ""), file)
elif event in ("scan_error", "scheduler_error"):
logger.error("[scan] %s", data.get("error", "") or data.get("file", ""))
elif event == "scan_done":
logger.info("[scan] Done — %d flagged, %d scanned",
data.get("flagged_count", 0), data.get("total_scanned", 0))
elif event == "scheduler_started":
logger.info("[scan] Scheduler started — %s", data.get("job_name", ""))
elif event == "scheduler_done":
logger.info("[scan] Scheduler done — %d flagged", data.get("flagged", 0))