/view, er ikke berørt.",
+ "interface_pin_clear": "Ryd PIN",
+ "interface_pin_is_set": "Interface-PIN er angivet",
+ "interface_pin_not_set_msg": "Ingen PIN angivet \u2014 grænsefladen er åben for alle på netværket",
+ "interface_pin_saved": "PIN gemt",
+ "interface_pin_clear_confirm": "Fjern interface-PIN? Scanneren vil herefter være tilgængelig for alle på netværket.",
+ "interface_pin_cleared": "PIN ryddet",
+ "interface_pin_login_desc": "Indtast interface-PIN for at fortsætte.",
+ "interface_pin_login_btn": "Fortsæt",
+ "interface_pin_err_incorrect": "Forkert PIN.",
+ "interface_pin_err_too_many": "For mange forsøg. Prøv igen om lidt.",
+ "interface_pin_err_network": "Netværksfejl. Prøv igen."
}
\ No newline at end of file
diff --git a/lang/de.json b/lang/de.json
index 1e13d77..b6ea9e6 100644
--- a/lang/de.json
+++ b/lang/de.json
@@ -669,7 +669,23 @@
"m365_smtp_test": "Testen",
"m365_smtp_testing": "Test-E-Mail wird gesendet…",
"m365_smtp_test_ok": "Test-E-Mail gesendet",
+ "m365_smtp_test_ok_graph": "Test-E-Mail über Microsoft Graph gesendet an",
+ "m365_smtp_test_ok_smtp": "Test-E-Mail über SMTP gesendet an",
+ "m365_smtp_graph_also_failed": "(⚠ Graph fehlgeschlagen — Mail.Send nicht erteilt)",
"m365_smtp_test_fail": "Verbindung fehlgeschlagen",
+ "bulk_select_mode": "Auswählen",
+ "bulk_select_all": "Alle sichtbaren auswählen",
+ "bulk_deselect_all": "Alle abwählen",
+ "bulk_apply": "Anwenden",
+ "bulk_done": "Fertig",
+ "bulk_selected": "ausgewählt",
+ "bulk_applied": "aktualisiert",
+ "disp_stats_total": "gesamt",
+ "disp_stats_unreviewed": "nicht überprüft",
+ "disp_stats_retain": "behalten",
+ "disp_stats_delete": "löschen",
+ "disp_stats_other": "sonstige",
+ "disp_stats_reviewed": "überprüft",
"m365_fsrc_edit_btn": "Bearbeiten",
"m365_fsrc_save_changes": "Änderungen speichern",
"m365_settings_tab_scheduler": "Zeitplaner",
@@ -793,5 +809,19 @@
"viewer_pin_saving": "Wird gespeichert…",
"viewer_pin_saved": "PIN gespeichert",
"viewer_pin_clear_confirm": "Betrachter-PIN entfernen? /view erfordert dann wieder einen Token-Link.",
- "viewer_pin_cleared": "PIN gelöscht"
+ "viewer_pin_cleared": "PIN gelöscht",
+
+ "interface_pin_group_title": "Interface-PIN",
+ "interface_pin_desc": "Eine numerische PIN (4\u20138 Stellen), die eingegeben werden muss, bevor auf die Scanner-Oberfläche zugegriffen werden kann. Betrachter, die /view aufrufen, sind nicht betroffen.",
+ "interface_pin_clear": "PIN löschen",
+ "interface_pin_is_set": "Interface-PIN ist gesetzt",
+ "interface_pin_not_set_msg": "Keine PIN gesetzt \u2014 Oberfläche ist für alle im Netzwerk offen",
+ "interface_pin_saved": "PIN gespeichert",
+ "interface_pin_clear_confirm": "Interface-PIN entfernen? Der Scanner ist dann für alle im Netzwerk zugänglich.",
+ "interface_pin_cleared": "PIN gelöscht",
+ "interface_pin_login_desc": "Interface-PIN eingeben, um fortzufahren.",
+ "interface_pin_login_btn": "Weiter",
+ "interface_pin_err_incorrect": "Falsche PIN.",
+ "interface_pin_err_too_many": "Zu viele Versuche. Bitte später erneut versuchen.",
+ "interface_pin_err_network": "Netzwerkfehler. Bitte erneut versuchen."
}
\ No newline at end of file
diff --git a/lang/en.json b/lang/en.json
index 374ca6d..9a0c7d1 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -669,7 +669,23 @@
"m365_smtp_test": "Test",
"m365_smtp_testing": "Sending test email…",
"m365_smtp_test_ok": "Test email sent",
+ "m365_smtp_test_ok_graph": "Test email sent via Microsoft Graph to",
+ "m365_smtp_test_ok_smtp": "Test email sent via SMTP to",
+ "m365_smtp_graph_also_failed": "(⚠ Graph also failed — Mail.Send not granted)",
"m365_smtp_test_fail": "Connection failed",
+ "bulk_select_mode": "Select",
+ "bulk_select_all": "Select all visible",
+ "bulk_deselect_all": "Deselect all",
+ "bulk_apply": "Apply",
+ "bulk_done": "Done",
+ "bulk_selected": "selected",
+ "bulk_applied": "updated",
+ "disp_stats_total": "total",
+ "disp_stats_unreviewed": "unreviewed",
+ "disp_stats_retain": "retain",
+ "disp_stats_delete": "delete",
+ "disp_stats_other": "other",
+ "disp_stats_reviewed": "reviewed",
"m365_fsrc_edit_btn": "Edit",
"m365_fsrc_save_changes": "Save changes",
"m365_settings_tab_scheduler": "Scheduler",
@@ -793,5 +809,19 @@
"viewer_pin_saving": "Saving\u2026",
"viewer_pin_saved": "PIN saved",
"viewer_pin_clear_confirm": "Remove the viewer PIN? /view will require a token link again.",
- "viewer_pin_cleared": "PIN cleared"
+ "viewer_pin_cleared": "PIN cleared",
+
+ "interface_pin_group_title": "Interface PIN",
+ "interface_pin_desc": "A numeric PIN (4\u20138 digits) that must be entered before accessing the main scanner interface. Viewers accessing /view are not affected.",
+ "interface_pin_clear": "Clear PIN",
+ "interface_pin_is_set": "Interface PIN is set",
+ "interface_pin_not_set_msg": "No PIN set \u2014 interface is open to anyone on the network",
+ "interface_pin_saved": "PIN saved",
+ "interface_pin_clear_confirm": "Remove the interface PIN? The scanner will be accessible to anyone on the network.",
+ "interface_pin_cleared": "PIN cleared",
+ "interface_pin_login_desc": "Enter the interface PIN to continue.",
+ "interface_pin_login_btn": "Continue",
+ "interface_pin_err_incorrect": "Incorrect PIN.",
+ "interface_pin_err_too_many": "Too many attempts. Try again later.",
+ "interface_pin_err_network": "Network error. Please try again."
}
\ No newline at end of file
diff --git a/routes/CLAUDE.md b/routes/CLAUDE.md
index 3e3db7c..2b96a5c 100644
--- a/routes/CLAUDE.md
+++ b/routes/CLAUDE.md
@@ -14,6 +14,9 @@ All three scan engines must include `"source": "m365"` / `"google"` / `"file"` i
## Circular import prohibition
`scan_engine.py` and `gdpr_scanner.py` must not import each other. `scan_engine` imports from `sse`, `checkpoint`, `app_config`, `cpr_detector`; `gdpr_scanner` imports scan functions from `scan_engine`.
+## `_scan_bytes` injection
+`scan_engine.py` declares stub versions of `_scan_bytes` / `_scan_bytes_timeout` at module level. `gdpr_scanner.py` replaces them with the real `cpr_detector` implementations at startup. `routes/google_scan.py` pulls them from `gdpr_scanner` via `__getattr__`. Never import these directly in blueprint or engine modules — that breaks the circular-import barrier.
+
## Gotchas
- **`_load_settings()` return** — does NOT include `file_sources`. Returns only: sources, user_ids, options, retention_years, fiscal_year_end, email_to.
diff --git a/routes/database.py b/routes/database.py
index f39258b..86182da 100644
--- a/routes/database.py
+++ b/routes/database.py
@@ -143,6 +143,26 @@ def db_set_disposition():
return jsonify({"status": "saved"})
+@bp.route("/api/db/disposition/bulk", methods=["POST"])
+def db_set_disposition_bulk():
+ """Set the same disposition on multiple items at once.
+ Body: {item_ids: [...], status, legal_basis?, notes?, reviewed_by?}
+ """
+ if not DB_OK: return jsonify({"error": "database not available"}), 503
+ data = request.get_json() or {}
+ item_ids = data.get("item_ids", [])
+ status = data.get("status", "")
+ if not item_ids or not status:
+ return jsonify({"error": "item_ids and status required"}), 400
+ db = _get_db()
+ for iid in item_ids:
+ db.set_disposition(iid, status,
+ legal_basis=data.get("legal_basis", ""),
+ notes=data.get("notes", ""),
+ reviewed_by=data.get("reviewed_by", ""))
+ return jsonify({"saved": len(item_ids)})
+
+
@bp.route("/api/db/disposition//view are not affected.{{ LANG.get('interface_pin_login_desc', 'Enter the interface PIN to continue.') }}
+ + + +