feat: scan history browser, user-scoped viewer tokens, export fixes, email fixes (v1.6.20)

- Scan history browser (history.js, GET /api/db/sessions, get_sessions(),
  get_session_items(ref_scan_id)) — review any past session without rescanning
- User-scoped viewer tokens (#34) — scope by individual employee across M365
  and GWS; autocomplete from Accounts list; dual-email support
- Fix: GWS scan never marked finished (end_scan → finish_scan) and emitted
  wrong SSE event (scan_done → google_scan_done), excluding GWS items from all
  exports
- Fix: file scan begin_scan called with wrong keyword args (TypeError swallowed),
  so local/SMB items were never written to DB
- Fix: Graph sendMail reported failure on success — _post() now returns {} on
  empty 202 response instead of raising JSONDecodeError
- Fix: Graph error hidden behind generic "No SMTP host" message when both Graph
  and SMTP were unavailable
- Fix: Gmail vs Google Workspace SMTP error messages distinguished by username
  domain; Workspace errors point to admin console, not personal security settings
- Docs: update README, MANUAL-EN, MANUAL-DA, CLAUDE.md, TODO.md, CHANGELOG.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
StyxX65andClaude Sonnet 4.6 committed 2026-04-18 13:57:54 +02:00
1 parent e64d7eb958
commit c9aab19a97
24 files changed
+753 -70

No files matched your search

+17 -1
View File
@@ -70,6 +70,13 @@ def db_scans():
return jsonify(_get_db().scans_list())
@bp.route("/api/db/sessions")
def db_sessions():
"""List scan sessions (grouped concurrent scans), newest first."""
if not DB_OK: return jsonify([])
return jsonify(_get_db().get_sessions())
@bp.route("/api/db/subject", methods=["POST"])
def db_subject_lookup():
"""Find all items containing a given CPR number.
@@ -154,13 +161,22 @@ def db_flagged_items():
from flask import session as _session
scope = _session.get("viewer_scope", {})
role_filt = scope.get("role", "") if isinstance(scope, dict) else ""
items = _get_db().get_session_items()
# user may be a list of emails (current) or a legacy single string
raw_user = scope.get("user", "") if isinstance(scope, dict) else ""
if isinstance(raw_user, list):
user_filt = set(e.lower() for e in raw_user if e)
else:
user_filt = {raw_user.lower()} if raw_user else set()
ref_scan_id = request.args.get("ref", type=int)
items = _get_db().get_session_items(ref_scan_id=ref_scan_id)
# Normalise JSON-encoded columns the same way scan_engine does for SSE cards
import json as _json
out = []
for row in items:
if role_filt and row.get("role", "") != role_filt:
continue
if user_filt and (row.get("account_id", "") or "").lower() not in user_filt:
continue
row["special_category"] = _json.loads(row.get("special_category") or "[]") if isinstance(row.get("special_category"), str) else row.get("special_category", [])
row["exif"] = _json.loads(row.get("exif_json") or "{}") if isinstance(row.get("exif_json"), str) else row.get("exif", {})
row.pop("exif_json", None)