v1.6.28 — Scheduled report-only jobs, compliance audit log, and documentation update
- Scheduled jobs can now run in report-only mode (skip scan, email latest DB results) - Compliance audit log records all significant admin actions in an immutable DB table - VERSION bumped to 1.6.28; CHANGELOG [Unreleased] sealed as [1.6.28] — 2026-05-28 - Both manuals updated: CPR-only mode, OCR language, file redaction, related documents, date-range token scoping, report-only jobs, audit log tab, two new FAQ entries - TODO.md updated with all completed tasks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
744813f4ac
commit
26c45165b9
@ -7,7 +7,7 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [Unreleased]
|
## [1.6.28] — 2026-05-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@ -21,6 +21,8 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
|
|||||||
|
|
||||||
- **`DELETE /api/delete_item` route registration fix** — the `delete_item` handler in `routes/export.py` was missing its `@bp.route` decorator, so the endpoint was never registered in Flask's URL map. The route now works correctly.
|
- **`DELETE /api/delete_item` route registration fix** — the `delete_item` handler in `routes/export.py` was missing its `@bp.route` decorator, so the endpoint was never registered in Flask's URL map. The route now works correctly.
|
||||||
|
|
||||||
|
- **Scheduled report-only email job** — scheduled jobs can now be configured as "report only" (toggle `#schedReportOnly`). When enabled, the job skips the scan entirely and instead emails the latest scan results already in the database. If the in-memory result list is empty (e.g. after a server restart), results are loaded from the DB via `get_session_items()`. M365 authentication is not required for report-only jobs — email is sent Graph-first if authenticated, SMTP otherwise. Jobs fail with a clear error if no scan results are available. The job list card shows a blue "Report only" badge. Setting `report_only=True` in the editor automatically enables "Email report automatically" and dims the Profile field (unused for report-only runs).
|
||||||
|
|
||||||
- **Compliance audit log** — every significant admin action is now written to an immutable `audit_log` table in the scanner database. Recorded events: profile save/delete, viewer token create/revoke, viewer/interface/admin PIN set/change/clear, file source add/update/delete, scheduler job save/delete, scan start/stop, SMTP config save, single and bulk disposition changes, item delete, and item redact. Each record stores a Unix timestamp, an action key, a human-readable detail string, and the client IP address. Accessible via `GET /api/audit_log` (returns newest-first, max 1000 entries; filterable by `?action=`). Visible in the Settings modal under a new **Audit Log** tab; the table refreshes whenever the tab is opened. The `log_audit_event()` module-level helper in `gdpr_db.py` silently no-ops if the DB is unavailable, so all call sites are safe in test and offline contexts.
|
- **Compliance audit log** — every significant admin action is now written to an immutable `audit_log` table in the scanner database. Recorded events: profile save/delete, viewer token create/revoke, viewer/interface/admin PIN set/change/clear, file source add/update/delete, scheduler job save/delete, scan start/stop, SMTP config save, single and bulk disposition changes, item delete, and item redact. Each record stores a Unix timestamp, an action key, a human-readable detail string, and the client IP address. Accessible via `GET /api/audit_log` (returns newest-first, max 1000 entries; filterable by `?action=`). Visible in the Settings modal under a new **Audit Log** tab; the table refreshes whenever the tab is opened. The `log_audit_event()` module-level helper in `gdpr_db.py` silently no-ops if the DB is unavailable, so all call sites are safe in test and offline contexts.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
11
CLAUDE.md
11
CLAUDE.md
@ -196,6 +196,17 @@ Allows reviewing results from any past scan session without running a new scan.
|
|||||||
- **Auto-email after manual scan** — `_maybe_send_auto_email()` in `routes/scan.py` is called from the `_run()` thread immediately after `run_scan()` returns. Reads `smtp_cfg.get("auto_email_manual")` from `smtp.json`; no-ops if the flag is false, no flagged items, or no recipients. Same Graph-first → SMTP-fallback pattern as the scheduler. Toggle: **Settings → Email report → Email report after manual scan** (`#st-smtpAutoEmail`), saved by `stSmtpSave()` in `scheduler.js`.
|
- **Auto-email after manual scan** — `_maybe_send_auto_email()` in `routes/scan.py` is called from the `_run()` thread immediately after `run_scan()` returns. Reads `smtp_cfg.get("auto_email_manual")` from `smtp.json`; no-ops if the flag is false, no flagged items, or no recipients. Same Graph-first → SMTP-fallback pattern as the scheduler. Toggle: **Settings → Email report → Email report after manual scan** (`#st-smtpAutoEmail`), saved by `stSmtpSave()` in `scheduler.js`.
|
||||||
- **Gmail vs Google Workspace detection** — auth error handlers check whether the SMTP username ends in `@gmail.com` / `@googlemail.com`. If not, the account is treated as Google Workspace (custom domain) and the error message points to the Workspace admin console rather than the user's personal security settings.
|
- **Gmail vs Google Workspace detection** — auth error handlers check whether the SMTP username ends in `@gmail.com` / `@googlemail.com`. If not, the account is treated as Google Workspace (custom domain) and the error message points to the Workspace admin console rather than the user's personal security settings.
|
||||||
|
|
||||||
|
## Scheduler — scan_scheduler.py + routes/scheduler.py + static/js/scheduler.js
|
||||||
|
|
||||||
|
- **Job config keys** — `id`, `name`, `enabled`, `frequency` (daily/weekly/monthly), `day_of_week`, `day_of_month`, `hour`, `minute`, `profile_id`, `auto_email`, `auto_retention`, `retention_years`, `fiscal_year_end`, `report_only`. Stored in `~/.gdprscanner/schedule.json`. Auto-migrates old single-job format; assigns UUIDs to legacy entries without one.
|
||||||
|
- **`_execute_scan(job_id)`** — the core execution method. Acquires a per-job lock (`_running_jobs` set), records a DB run via `db.begin_schedule_run()`, then either takes the report-only path (see below) or runs the full scan pipeline (M365 → file → Google), then emails and applies retention if configured. The DB run is finalised in a `finally` block so status/counts are always recorded.
|
||||||
|
- **Report-only path** — when `report_only=True`, `_execute_scan` short-circuits before the M365 auth check. It populates `_m.flagged_items` from `db.get_session_items()` if the in-memory list is empty, then calls `_send_email_report(job_cfg)` and returns. Does NOT acquire the scan lock; does NOT require M365 auth. Fails with `RuntimeError("No scan results available")` if both in-memory state and the DB are empty, which the outer `except` handler records as a failed run.
|
||||||
|
- **`_send_email_report(job_cfg)`** — builds Excel via `_m._build_excel_bytes()`, loads SMTP config, tries Graph first (if `state.connector` is authenticated), falls back to SMTP. Adjusts the email body text based on `job_cfg.get("report_only")`: "Scan completed" vs "Report on latest scan results".
|
||||||
|
- **`_m.flagged_items` and `state.flagged_items` are the same object** — `gdpr_scanner.py` assigns `_state.flagged_items = flagged_items` at startup, so both names reference the same list. In-place updates (`flagged_items[:] = ...`) in the scheduler propagate to routes and vice versa.
|
||||||
|
- **`scheduler_started` / `scheduler_done` SSE events** — broadcast at start and end of every job (including report-only). `scheduler_done` carries `flagged`, `scanned`, `emailed`, and `job_name`. Do not confuse with `scan_done` (M365) — they are separate event types.
|
||||||
|
- **UI — job card badge** — `schedRenderJobs()` in `scheduler.js` adds a blue "Report only" (`m365_sched_report_only`) badge to the job name when `j.report_only` is true.
|
||||||
|
- **UI — `schedToggleReportOnly()`** — dims the Profile row (`#schedProfileRow` opacity 0.4), shows/hides `#schedReportOnlyHint`, and forces `#schedAutoEmail` checked. Called from the checkbox `onchange` handler and at the start of `schedAddJob()` / `schedEditJob()`.
|
||||||
|
|
||||||
## Global gotchas
|
## Global gotchas
|
||||||
|
|
||||||
- **Pattern matching in Python** — when using `str.replace()` to patch JS/HTML, whitespace and quote style must match exactly. Use `in` check first and print if not found.
|
- **Pattern matching in Python** — when using `str.replace()` to patch JS/HTML, whitespace and quote style must match exactly. Use `in` check first and print if not found.
|
||||||
|
|||||||
60
TODO.md
60
TODO.md
@ -111,11 +111,67 @@ Optional session-level authentication gate for the main scanner interface. Set i
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### SFTP as a 4th file connector 🔄 In progress
|
### OCR language override ✅
|
||||||
|
Tesseract language pack(s) used for scanned PDFs and images are now configurable per profile. Option `ocr_lang` (default `dan+eng`). Presets: `dan+eng`, `dan`, `eng`, `dan+eng+deu`, `dan+eng+swe`, `dan+eng+fra`. Threaded through `_scan_bytes`/`_scan_bytes_timeout` → `document_scanner.scan_pdf`/`scan_image` and the spawned PDF-OCR subprocess. OCR result cache keys include `lang` so per-language results are cached independently. Sidebar select `#optOcrLang`; profile editor `#peOptOcrLang`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CPR-only mode ✅
|
||||||
|
New scan option `cpr_only` (default `false`). When enabled, items whose only hits are email addresses, phone numbers, detected faces, or EXIF/GPS metadata are skipped — only items with at least one qualifying CPR number are flagged. Implemented as a compact short-circuit at each engine's flagging gate. Sidebar toggle `#optCprOnly`; profile editor `#peOptCprOnly`.
|
||||||
|
|
||||||
|
Also added `min_cpr_count` (default `1`) — minimum number of **distinct** CPR numbers required before a file is flagged. Files with faces or EXIF PII are still flagged regardless of this threshold.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Skip GPS images ✅
|
||||||
|
Scan option `skip_gps_images` (default `false`). When enabled, images whose only PII is GPS coordinates are not flagged. GPS data is still stored in the card `exif` field if the item is flagged by another signal. Sidebar toggle `#optSkipGps`; profile editor `#peOptSkipGps`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CPR cross-referencing (related documents) ✅
|
||||||
|
The preview panel now shows a "Related documents" section listing other items in the same scan session that share ≥1 CPR number. Clicking any related item opens its preview. Implemented as a query-time self-join on the existing `cpr_index` table — no new data collection needed. `GET /api/db/related/<item_id>?ref=N` returns rows ordered by shared CPR count descending.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Email preview on checkpoint resume ✅
|
||||||
|
A 500-character plain-text body excerpt (`body_excerpt`) is now stored per flagged email at broadcast time and persisted in the DB. When the preview modal opens for an email item, this excerpt is shown immediately without requiring a live Graph/Gmail connection. Enables email preview to work correctly after a server restart and checkpoint resume.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Built-in file redaction ✅
|
||||||
|
Local files (`.docx`, `.xlsx`, `.csv`, `.txt`) can be redacted in-place: CPR numbers are replaced by `██████-████` / `█` blocks, the card is removed from the grid, and a `"redacted"` disposition is logged. The ✂ button appears on redactable local file cards (hidden in viewer mode and for resolved items). File is written to a temp path in the same directory before `shutil.move` to avoid cross-device rename failures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Date-range scoping for viewer tokens ✅
|
||||||
|
Viewer tokens can now carry `valid_from` and/or `valid_to` fields (YYYY-MM-DD). `GET /api/db/flagged` filters out items whose `modified` date falls outside the range. All three scope dimensions (role, user, date-range) are independent and combinable. The share modal exposes `#shareValidFrom` / `#shareValidTo` date inputs. Token list shows a green date-range badge when a range is present.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Re-scan diff ✅
|
||||||
|
When viewing a history session, items present in the immediately preceding session but absent from the current one are shown below a `.resolved-divider` separator with a green ✓ Resolved badge (opacity dimmed). These resolved items are grid-only — they are not added to `S.flaggedData` and cannot be bulk-selected or exported. The history banner shows a resolved count when applicable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tests for Google Workspace scan engine ✅
|
||||||
|
19 tests added in `tests/test_google_scan.py` covering: `GET /api/google/scan/users`, `POST /api/google/scan/start`, `POST /api/google/scan/cancel`, and `_run_google_scan` engine internals. Uses synchronous invocation with mocked `broadcast`, `_scan_bytes`, `checkpoint.*`, and `gdpr_db.get_db`. The `clean_google_state` autouse fixture releases `_google_scan_lock` and clears `_google_scan_abort` after each test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Compliance audit log ✅
|
||||||
|
Every significant admin action is written to an immutable `audit_log` table in the scanner database. Recorded events: profile save/delete, viewer token create/revoke, viewer/interface/admin PIN set/change/clear, file source add/update/delete, scheduler job save/delete, scan start/stop, SMTP config save, single and bulk disposition changes, item delete, and item redact. Each record stores a Unix timestamp, action key, human-readable detail, and client IP. `GET /api/audit_log` returns newest-first (max 1000; filterable by `?action=`). Visible in Settings → **Audit Log** tab; refreshes when the tab is opened. `log_audit_event()` helper in `gdpr_db.py` silently no-ops if the DB is unavailable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scheduled report-only email job ✅
|
||||||
|
Scheduler jobs can now be configured as "report only" (toggle `#schedReportOnly`). The job skips the scan entirely and emails the latest results already in the database. If the in-memory result list is empty (e.g. after a server restart), results are loaded from DB via `get_session_items()`. M365 auth is not required — email is sent Graph-first if authenticated, SMTP otherwise. Jobs fail with a clear error if no scan results are available. The job list card shows a blue "Report only" badge. Enabling report-only automatically checks "Email report automatically" and dims the Profile field (unused for report-only runs).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SFTP as a 4th file connector ✅
|
||||||
Scan SFTP servers (SSH File Transfer Protocol) alongside local, SMB, and cloud sources. A new `SFTPScanner` class in `sftp_connector.py` implements the same `iter_files()` interface as `FileScanner`, so `run_file_scan()` and everything downstream (SSE, DB, export, scheduling) is unchanged. Auth supports password and SSH private key (+ optional passphrase). Key files stored in `~/.gdprscanner/sftp_keys/`. SFTP sources appear in the file sources panel with a 🔒 icon, are profile-aware, and are included in scheduled scans automatically.
|
Scan SFTP servers (SSH File Transfer Protocol) alongside local, SMB, and cloud sources. A new `SFTPScanner` class in `sftp_connector.py` implements the same `iter_files()` interface as `FileScanner`, so `run_file_scan()` and everything downstream (SSE, DB, export, scheduling) is unchanged. Auth supports password and SSH private key (+ optional passphrase). Key files stored in `~/.gdprscanner/sftp_keys/`. SFTP sources appear in the file sources panel with a 🔒 icon, are profile-aware, and are included in scheduled scans automatically.
|
||||||
|
|
||||||
**Files changed:** `sftp_connector.py` (new), `scan_engine.py`, `routes/sources.py`, `app_config.py`, `static/js/sources.js`, `templates/index.html`, `lang/en|da|de.json`, `routes/export.py`, `requirements.txt`
|
**Files changed:** `sftp_connector.py` (new), `scan_engine.py`, `routes/sources.py`, `app_config.py`, `static/js/sources.js`, `templates/index.html`, `lang/en|da|de.json`, `routes/export.py`, `requirements.txt`
|
||||||
**Size:** Medium · **Priority:** Medium
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# GDPR Scanner — Brugermanual
|
# GDPR Scanner — Brugermanual
|
||||||
|
|
||||||
Version 1.6.25
|
Version 1.6.28
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -170,6 +170,10 @@ Scan kun elementer ændret efter en bestemt dato. Hurtige forudindstillinger —
|
|||||||
|
|
||||||
**Maks. e-mails pr. bruger** — stop efter at have scannet dette antal e-mails per person (standard 2.000). Øg det, hvis du har brug for fuld dækning.
|
**Maks. e-mails pr. bruger** — stop efter at have scannet dette antal e-mails per person (standard 2.000). Øg det, hvis du har brug for fuld dækning.
|
||||||
|
|
||||||
|
**Kun CPR-tilstand** — når aktiveret, flagges kun elementer, der indeholder mindst ét kvalificerende CPR-nummer. Elementer, hvis eneste fund er e-mailadresser, telefonnumre, ansigter eller GPS/EXIF-metadata, springes over. Nyttigt, når du ønsker en fokuseret rapport udelukkende om CPR-eksponering.
|
||||||
|
|
||||||
|
**OCR-sprog** — vælg den sprogpakke, Tesseract bruger, når der læses tekst fra scannede PDF-filer og billeder. Standard er `Dansk + Engelsk`, som dækker langt de fleste dokumenter. Skift til en anden forudindstilling, hvis dine dokumenter overvejende er på et andet sprog.
|
||||||
|
|
||||||
### 4.4 Start scanningen
|
### 4.4 Start scanningen
|
||||||
|
|
||||||
Klik på den blå **Scan**-knap i topbjælken.
|
Klik på den blå **Scan**-knap i topbjælken.
|
||||||
@ -270,6 +274,7 @@ Forhåndsvisningen viser:
|
|||||||
- Alle fundne CPR-numre og deres kontekst
|
- Alle fundne CPR-numre og deres kontekst
|
||||||
- Øvrige personoplysninger registreret (telefon, e-mailadresse, IBAN mv.)
|
- Øvrige personoplysninger registreret (telefon, e-mailadresse, IBAN mv.)
|
||||||
- Deling og ekstern adgangsinformation
|
- Deling og ekstern adgangsinformation
|
||||||
|
- **Relaterede dokumenter** — hvis andre elementer i samme scanningssession indeholder ét eller flere af de samme CPR-numre, vises de i et "Relaterede dokumenter"-afsnit. Klik på et element for at åbne dets forhåndsvisning. Det gør det nemmere at spore en persons data på tværs af flere filer eller e-mails.
|
||||||
|
|
||||||
### Angiv en disposition
|
### Angiv en disposition
|
||||||
|
|
||||||
@ -287,6 +292,10 @@ Hvert element har en **Disposition**-rullemenu i forhåndsvisningspanelet. Vælg
|
|||||||
|
|
||||||
Klik på **Gem** efter valget. En lille **✓ Gemt**-bekræftelse vises.
|
Klik på **Gem** efter valget. En lille **✓ Gemt**-bekræftelse vises.
|
||||||
|
|
||||||
|
### Redigér en lokal fil
|
||||||
|
|
||||||
|
For lokale DOCX-, XLSX-, CSV- og TXT-filer vises en **✂**-knap på kortet. Klikker du på den, overskrives filen på stedet, og alle CPR-numre erstattes med `██████-████`-blokke. Kortet fjernes fra gitteret, og handlingen registreres som en `"redacted"`-disposition. Brug denne mulighed, når du ønsker at anonymisere en fil frem for at slette den helt. Knappen er ikke tilgængelig for e-mails, cloud-filer eller SFTP-filer.
|
||||||
|
|
||||||
### Massemarkering af flere elementer på én gang
|
### Massemarkering af flere elementer på én gang
|
||||||
|
|
||||||
Hvis du skal anvende den samme disposition på mange elementer, kan du bruge **Vælg-tilstand** i stedet for at åbne hvert kort enkeltvis.
|
Hvis du skal anvende den samme disposition på mange elementer, kan du bruge **Vælg-tilstand** i stedet for at åbne hvert kort enkeltvis.
|
||||||
@ -408,9 +417,10 @@ Klik på **🔗**-knappen øverst til højre i topbjælken for at åbne delingsp
|
|||||||
- **Alle roller** — modtageren ser alle fundne elementer.
|
- **Alle roller** — modtageren ser alle fundne elementer.
|
||||||
- **Ansatte** / **Elever** — modtageren ser kun elementer tilhørende den valgte rollegruppe. Rollefilteret er låst i deres visning.
|
- **Ansatte** / **Elever** — modtageren ser kun elementer tilhørende den valgte rollegruppe. Rollefilteret er låst i deres visning.
|
||||||
- **Bruger** — modtageren ser kun elementer tilhørende en bestemt medarbejder. Vælg personen fra søgefeltet; scanneren matcher automatisk både deres M365- og Google Workspace-e-mailadresser. Brug denne mulighed, når du vil give en enkelt medarbejder adgang til sine egne scanningsresultater.
|
- **Bruger** — modtageren ser kun elementer tilhørende en bestemt medarbejder. Vælg personen fra søgefeltet; scanneren matcher automatisk både deres M365- og Google Workspace-e-mailadresser. Brug denne mulighed, når du vil give en enkelt medarbejder adgang til sine egne scanningsresultater.
|
||||||
3. Vælg en **Udløbsdato** — 7 dage, 30 dage, 90 dage, 1 år eller Aldrig.
|
3. Angiv eventuelt et **Datointerval** — brug felterne "Elementer fra" og "Elementer til" for at begrænse modtagerens visning til elementer ændret inden for en bestemt periode. Lad begge felter stå tomme for ingen datobegrænsning.
|
||||||
4. Klik på **Opret**. Der genereres et unikt link: `http://host:5100/view?token=…`
|
4. Vælg en **Udløbsdato** — 7 dage, 30 dage, 90 dage, 1 år eller Aldrig.
|
||||||
5. Klik på **Kopiér** for at kopiere linket til udklipsholderen, og send det til gennemgangeren.
|
5. Klik på **Opret**. Der genereres et unikt link: `http://host:5100/view?token=…`
|
||||||
|
6. Klik på **Kopiér** for at kopiere linket til udklipsholderen, og send det til gennemgangeren.
|
||||||
|
|
||||||
Gennemgangeren åbner linket i en browser. De kan se resultatgitteret (afgrænset til det tilladte rolleomfang) og mærke dispositioner, men kan ikke starte scanninger, ændre indstillinger, se loginoplysninger eller slette elementer.
|
Gennemgangeren åbner linket i en browser. De kan se resultatgitteret (afgrænset til det tilladte rolleomfang) og mærke dispositioner, men kan ikke starte scanninger, ændre indstillinger, se loginoplysninger eller slette elementer.
|
||||||
|
|
||||||
@ -462,6 +472,7 @@ Gå til **Indstillinger → Planlægger** for at konfigurere automatiske scannin
|
|||||||
7. Aktiver eventuelt:
|
7. Aktiver eventuelt:
|
||||||
- **Send rapport automatisk** — send Excel-rapporten pr. e-mail til dine konfigurerede modtagere efter hver scanning.
|
- **Send rapport automatisk** — send Excel-rapporten pr. e-mail til dine konfigurerede modtagere efter hver scanning.
|
||||||
- **Håndhæv opbevaringspolitik** — slet automatisk elementer ældre end din opbevaringspolitik efter hver scanning.
|
- **Håndhæv opbevaringspolitik** — slet automatisk elementer ældre end din opbevaringspolitik efter hver scanning.
|
||||||
|
- **Kun rapport** — spring scanningen over og send blot de seneste resultater fra databasen som e-mail. Nyttigt til regelmæssige opsummerings-e-mails uden at køre en ny scanning. Når aktiveret, kræves ingen profil, og M365-godkendelse er ikke nødvendig.
|
||||||
8. Klik på **Gem**.
|
8. Klik på **Gem**.
|
||||||
|
|
||||||
Planlæggerikatoren i topbjælken viser dato og tidspunkt for den næste planlagte scanning ("Næste: …").
|
Planlæggerikatoren i topbjælken viser dato og tidspunkt for den næste planlagte scanning ("Næste: …").
|
||||||
@ -554,6 +565,10 @@ Disse indstillinger findes i venstre panel under **Indstillinger**:
|
|||||||
|
|
||||||
**Min. CPR-antal pr. fil** — en fil flagges kun, hvis den indeholder mindst dette antal *distinkte* CPR-numre. Standardværdien er 1 (nuværende adfærd). Sæt til 2 for at undgå falske positive ved elevscanninger: en elevs samtykkeerklæring eller indmeldelsesformular indeholder typisk kun elevens eget CPR-nummer, mens en klasselist eller karakteroversigt med flere elevers CPR-numre stadig vil blive rapporteret.
|
**Min. CPR-antal pr. fil** — en fil flagges kun, hvis den indeholder mindst dette antal *distinkte* CPR-numre. Standardværdien er 1 (nuværende adfærd). Sæt til 2 for at undgå falske positive ved elevscanninger: en elevs samtykkeerklæring eller indmeldelsesformular indeholder typisk kun elevens eget CPR-nummer, mens en klasselist eller karakteroversigt med flere elevers CPR-numre stadig vil blive rapporteret.
|
||||||
|
|
||||||
|
**Kun CPR-tilstand** — når aktiveret, springes elementer uden CPR-numre over (kun e-mailadresser, telefonnumre, ansigter eller GPS/EXIF-data). Brug dette, når du ønsker en rapport, der udelukkende fokuserer på CPR-eksponering.
|
||||||
|
|
||||||
|
**OCR-sprog** — vælger den sprogpakke, Tesseract bruger, når der læses tekst fra scannede PDF-filer og billeder. Standard: `Dansk + Engelsk`. Skift til en anden forudindstilling for dokumenter på tysk, svensk eller fransk.
|
||||||
|
|
||||||
**Opbevaringspolitik** — når aktiveret, markeres elementer ældre end det angivne antal år som forældet. Regnskabsårets afslutning bestemmer, hvordan skæringsdatoen beregnes:
|
**Opbevaringspolitik** — når aktiveret, markeres elementer ældre end det angivne antal år som forældet. Regnskabsårets afslutning bestemmer, hvordan skæringsdatoen beregnes:
|
||||||
|
|
||||||
| Indstilling | Beregning af skæringsdato |
|
| Indstilling | Beregning af skæringsdato |
|
||||||
@ -562,6 +577,12 @@ Disse indstillinger findes i venstre panel under **Indstillinger**:
|
|||||||
| 31 dec (Bogføringsloven) | Seneste 31. december minus N år |
|
| 31 dec (Bogføringsloven) | Seneste 31. december minus N år |
|
||||||
| 30 jun / 31 mar | Seneste forekomst af den dato minus N år |
|
| 30 jun / 31 mar | Seneste forekomst af den dato minus N år |
|
||||||
|
|
||||||
|
### Fanen Revisionslog
|
||||||
|
|
||||||
|
Gå til **Indstillinger → Revisionslog** for at se en uforanderlig log over alle væsentlige administrative handlinger i scanneren. Hver post viser tidspunkt, handlingstype, detaljer og klientens IP-adresse. Registrerede hændelser omfatter: gem/slet profil, opret/tilbagekald viewer-token, PIN-ændringer, tilføj/opdater/slet filkilde, gem/slet planlagt job, start/stop scanning, gem SMTP-konfiguration, dispositionsændringer, slet element og redigér element.
|
||||||
|
|
||||||
|
Loggen er skrivebeskyttet og gemmes i scannerdatabasen sammen med scanningsresultaterne. Den er inkluderet i databaseeksporter og kan hjælpe dig med at dokumentere ansvarlighed over for en tilsynsmyndighed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 15. Ofte stillede spørgsmål
|
## 15. Ofte stillede spørgsmål
|
||||||
@ -599,6 +620,12 @@ Ja. Gå til **Indstillinger → Sikkerhed → Interface-PIN** og angiv en 4–8-
|
|||||||
**Kan en gennemganger mærke dispositioner uden adgang til scanningskontrollerne?**
|
**Kan en gennemganger mærke dispositioner uden adgang til scanningskontrollerne?**
|
||||||
Ja. Brug **🔗 Del**-knappen til at oprette et skrivebeskyttet viewer-link eller angiv en Viewer-PIN under Indstillinger → Sikkerhed. Gennemgangeren åbner linket i sin browser og kan gennemse resultater og mærke dispositioner uden at se loginoplysninger, kilder eller scanningsknapper. Se afsnit 10 for detaljer.
|
Ja. Brug **🔗 Del**-knappen til at oprette et skrivebeskyttet viewer-link eller angiv en Viewer-PIN under Indstillinger → Sikkerhed. Gennemgangeren åbner linket i sin browser og kan gennemse resultater og mærke dispositioner uden at se loginoplysninger, kilder eller scanningsknapper. Se afsnit 10 for detaljer.
|
||||||
|
|
||||||
|
**Kan jeg begrænse et delelink til en bestemt tidsperiode?**
|
||||||
|
Ja. Brug felterne "Elementer fra" og "Elementer til" i delingspanelet, når du opretter et token-link. Modtageren vil kun se elementer, hvis ændringsdate falder inden for det angivne interval.
|
||||||
|
|
||||||
|
**Hvor kan jeg se, hvem der har ændret hvad i scanneren?**
|
||||||
|
Gå til **Indstillinger → Revisionslog**. Alle væsentlige administrative handlinger logges med tidsstempel, handlingstype, detaljer og IP-adresse.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*GDPR Scanner v1.6.25 — teknisk opsætning og konfiguration: se README.md*
|
*GDPR Scanner v1.6.28 — teknisk opsætning og konfiguration: se README.md*
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# GDPR Scanner — User Manual
|
# GDPR Scanner — User Manual
|
||||||
|
|
||||||
Version 1.6.25
|
Version 1.6.28
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -170,6 +170,10 @@ Only scan items modified after a certain date. Quick presets — **1 år**, **2
|
|||||||
|
|
||||||
**Max emails per user** — stop after scanning this many emails per person (default 2,000). Increase if you need complete coverage.
|
**Max emails per user** — stop after scanning this many emails per person (default 2,000). Increase if you need complete coverage.
|
||||||
|
|
||||||
|
**CPR-only mode** — when enabled, only items containing at least one qualifying CPR number are flagged. Items whose only hits are email addresses, phone numbers, detected faces, or EXIF/GPS metadata are skipped. Useful when you want a focused CPR-only report without noise from other data types.
|
||||||
|
|
||||||
|
**OCR language** — choose the language pack(s) Tesseract uses when reading text from scanned PDFs and images. The default `Danish + English` covers the vast majority of documents. Switch to a different preset if your documents are predominantly in another language.
|
||||||
|
|
||||||
### 4.4 Start the Scan
|
### 4.4 Start the Scan
|
||||||
|
|
||||||
Click the blue **Scan** button in the top bar.
|
Click the blue **Scan** button in the top bar.
|
||||||
@ -270,6 +274,7 @@ The preview shows:
|
|||||||
- All CPR numbers found and their context
|
- All CPR numbers found and their context
|
||||||
- Other personal data detected (phone, email address, IBAN, etc.)
|
- Other personal data detected (phone, email address, IBAN, etc.)
|
||||||
- Sharing and external-access information
|
- Sharing and external-access information
|
||||||
|
- **Related documents** — if other items in the same scan session share one or more CPR numbers with this item, a "Related documents" section lists them. Click any row to open that item's preview. This helps you track the same person's data across multiple files or emails.
|
||||||
|
|
||||||
### Setting a disposition
|
### Setting a disposition
|
||||||
|
|
||||||
@ -285,7 +290,11 @@ Every item has a **Disposition** dropdown in the preview panel. Choose one of:
|
|||||||
| Privat brug — uden for scope | Personal item, not in scope for GDPR processing |
|
| Privat brug — uden for scope | Personal item, not in scope for GDPR processing |
|
||||||
| Slettet | Already deleted (set automatically when you delete an item) |
|
| Slettet | Already deleted (set automatically when you delete an item) |
|
||||||
|
|
||||||
After choosing, click **Gem**. A small **✓ Gemt** confirmation appears.
|
After choosing, click **Save**. A small **✓ Saved** confirmation appears.
|
||||||
|
|
||||||
|
### Redacting a local file
|
||||||
|
|
||||||
|
For local DOCX, XLSX, CSV, and TXT files a **✂** button appears in the card. Clicking it rewrites the file in-place, replacing all CPR numbers with `██████-████` blocks. The card is removed from the grid and the action is logged as a `"redacted"` disposition. This is useful when you want to sanitise a file rather than delete it entirely. The button is not available for email items, cloud files, or SFTP files.
|
||||||
|
|
||||||
### Bulk tagging multiple items at once
|
### Bulk tagging multiple items at once
|
||||||
|
|
||||||
@ -408,9 +417,10 @@ Click the **🔗** button in the top-right of the top bar to open the Share pane
|
|||||||
- **All roles** — the recipient sees all flagged items.
|
- **All roles** — the recipient sees all flagged items.
|
||||||
- **Ansatte** / **Elever** — the recipient sees only items belonging to that role group. The role filter is locked in their view.
|
- **Ansatte** / **Elever** — the recipient sees only items belonging to that role group. The role filter is locked in their view.
|
||||||
- **User** — the recipient sees only the items belonging to a specific employee. Select the person from the search box; the scanner matches both their M365 and Google Workspace email addresses automatically. Use this when you want to give an individual employee access to their own scan results.
|
- **User** — the recipient sees only the items belonging to a specific employee. Select the person from the search box; the scanner matches both their M365 and Google Workspace email addresses automatically. Use this when you want to give an individual employee access to their own scan results.
|
||||||
3. Choose an **Expiry** — 7 days, 30 days, 90 days, 1 year, or Never.
|
3. Optionally set a **Date range** — use the "Items from" and "Items until" date fields to limit the recipient to items modified within a specific period. This lets you, for example, create a link covering only last year's scan results. Leave both fields blank for no date restriction.
|
||||||
4. Click **Create**. A unique link is generated: `http://host:5100/view?token=…`
|
4. Choose an **Expiry** — 7 days, 30 days, 90 days, 1 year, or Never.
|
||||||
5. Click **Copy** to copy the link to your clipboard, then send it to the reviewer.
|
5. Click **Create**. A unique link is generated: `http://host:5100/view?token=…`
|
||||||
|
6. Click **Copy** to copy the link to your clipboard, then send it to the reviewer.
|
||||||
|
|
||||||
The reviewer opens the link in any browser. They see the results grid (filtered to their permitted scope) and can tag dispositions but cannot start scans, change settings, view credentials, or delete items.
|
The reviewer opens the link in any browser. They see the results grid (filtered to their permitted scope) and can tag dispositions but cannot start scans, change settings, view credentials, or delete items.
|
||||||
|
|
||||||
@ -462,6 +472,7 @@ Go to **Settings → Planlægger** to configure automatic scans.
|
|||||||
7. Optionally enable:
|
7. Optionally enable:
|
||||||
- **Send rapport automatisk** — email the Excel report to your configured recipients after each scan.
|
- **Send rapport automatisk** — email the Excel report to your configured recipients after each scan.
|
||||||
- **Håndhæv opbevaringspolitik** — automatically delete items older than your retention policy after each scan.
|
- **Håndhæv opbevaringspolitik** — automatically delete items older than your retention policy after each scan.
|
||||||
|
- **Report only** — skip the scan entirely and just email the latest results already in the database. Useful for sending a regular summary email without running a new scan. When enabled, no profile is needed and M365 authentication is not required.
|
||||||
8. Click **Gem** (Save).
|
8. Click **Gem** (Save).
|
||||||
|
|
||||||
The scheduler indicator in the top bar shows the date and time of the next scheduled scan ("Next: …").
|
The scheduler indicator in the top bar shows the date and time of the next scheduled scan ("Next: …").
|
||||||
@ -554,6 +565,10 @@ These options are in the left sidebar under **Indstillinger**:
|
|||||||
|
|
||||||
**Min. CPR count per file** — only flag a file if it contains at least this many *distinct* CPR numbers. The default is 1 (current behaviour). Setting it to 2 avoids false positives in student scans: a student's own consent form or registration document typically contains only their own CPR number, while a class list or grade sheet containing multiple students' CPRs will still be reported.
|
**Min. CPR count per file** — only flag a file if it contains at least this many *distinct* CPR numbers. The default is 1 (current behaviour). Setting it to 2 avoids false positives in student scans: a student's own consent form or registration document typically contains only their own CPR number, while a class list or grade sheet containing multiple students' CPRs will still be reported.
|
||||||
|
|
||||||
|
**CPR-only mode** — when enabled, items with no CPR numbers (only email addresses, phone numbers, faces, or GPS/EXIF data) are skipped entirely. Use this when you want a lean report focused exclusively on CPR exposure.
|
||||||
|
|
||||||
|
**OCR language** — selects the Tesseract language pack(s) used when reading scanned PDFs and images. Default: `Danish + English`. Change to a different preset if your documents are in another language (German, Swedish, French presets are available).
|
||||||
|
|
||||||
**Retention policy** — when enabled, marks items older than the specified number of years as overdue. The fiscal year end setting determines how the cutoff date is calculated:
|
**Retention policy** — when enabled, marks items older than the specified number of years as overdue. The fiscal year end setting determines how the cutoff date is calculated:
|
||||||
|
|
||||||
| Option | Cutoff date calculation |
|
| Option | Cutoff date calculation |
|
||||||
@ -562,6 +577,12 @@ These options are in the left sidebar under **Indstillinger**:
|
|||||||
| 31 dec (Bogføringsloven) | Last 31 December minus N years |
|
| 31 dec (Bogføringsloven) | Last 31 December minus N years |
|
||||||
| 30 jun / 31 mar | Last occurrence of that date minus N years |
|
| 30 jun / 31 mar | Last occurrence of that date minus N years |
|
||||||
|
|
||||||
|
### Audit Log tab
|
||||||
|
|
||||||
|
Go to **Settings → Audit Log** to view an immutable log of all significant admin actions performed in the scanner. Each entry shows the time, action type, detail, and client IP address. Recorded events include: profile save/delete, viewer token create/revoke, PIN changes, file source add/update/delete, scheduler job save/delete, scan start/stop, SMTP config save, dispositions, item delete, and item redact.
|
||||||
|
|
||||||
|
The log is read-only and is stored in the scanner database alongside scan results. It is included in database exports and can help you demonstrate accountability to a supervisory authority.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 15. Frequently Asked Questions
|
## 15. Frequently Asked Questions
|
||||||
@ -599,6 +620,12 @@ Yes. Go to **Settings → Security → Interface PIN** and set a 4–8 digit PIN
|
|||||||
**Can a reviewer tag dispositions without access to the scan controls?**
|
**Can a reviewer tag dispositions without access to the scan controls?**
|
||||||
Yes. Use the **🔗 Share** button to create a read-only viewer link or set a Viewer PIN in Settings → Security. The reviewer opens the link in their browser and can browse results and tag dispositions without seeing credentials, sources, or scan buttons. See section 10 for details.
|
Yes. Use the **🔗 Share** button to create a read-only viewer link or set a Viewer PIN in Settings → Security. The reviewer opens the link in their browser and can browse results and tag dispositions without seeing credentials, sources, or scan buttons. See section 10 for details.
|
||||||
|
|
||||||
|
**Can I limit a reviewer's link to a specific time period?**
|
||||||
|
Yes. When creating a token link, use the "Items from" and "Items until" date fields to restrict the link to items modified within that range. The reviewer will only see items whose modification date falls within the window you specified.
|
||||||
|
|
||||||
|
**Where can I see who changed what in the scanner?**
|
||||||
|
Go to **Settings → Audit Log**. Every significant admin action is recorded there with a timestamp, action type, detail, and IP address.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*GDPR Scanner v1.6.25 — for technical setup and configuration see README.md*
|
*GDPR Scanner v1.6.28 — for technical setup and configuration see README.md*
|
||||||
|
|||||||
@ -753,6 +753,8 @@
|
|||||||
"m365_sched_after_scan": "Efter scanning",
|
"m365_sched_after_scan": "Efter scanning",
|
||||||
"m365_sched_auto_email": "Send rapport automatisk",
|
"m365_sched_auto_email": "Send rapport automatisk",
|
||||||
"m365_sched_auto_retention": "Håndhæv opbevaringspolitik",
|
"m365_sched_auto_retention": "Håndhæv opbevaringspolitik",
|
||||||
|
"m365_sched_report_only": "Kun rapport",
|
||||||
|
"m365_sched_report_only_hint": "Send de seneste scanningsresultater uden at køre en ny scanning. Kræver scanningsresultater i databasen.",
|
||||||
"m365_sched_status": "Status",
|
"m365_sched_status": "Status",
|
||||||
"m365_sched_run_now": "▶ Kør nu",
|
"m365_sched_run_now": "▶ Kør nu",
|
||||||
"m365_sched_add": "+ Tilføj planlagt scanning",
|
"m365_sched_add": "+ Tilføj planlagt scanning",
|
||||||
|
|||||||
@ -753,6 +753,8 @@
|
|||||||
"m365_sched_after_scan": "Nach dem Scan",
|
"m365_sched_after_scan": "Nach dem Scan",
|
||||||
"m365_sched_auto_email": "Bericht automatisch senden",
|
"m365_sched_auto_email": "Bericht automatisch senden",
|
||||||
"m365_sched_auto_retention": "Aufbewahrungsrichtlinie durchsetzen",
|
"m365_sched_auto_retention": "Aufbewahrungsrichtlinie durchsetzen",
|
||||||
|
"m365_sched_report_only": "Nur Bericht",
|
||||||
|
"m365_sched_report_only_hint": "Letzte Scanergebnisse senden, ohne einen neuen Scan durchzuführen. Erfordert Scanergebnisse in der Datenbank.",
|
||||||
"m365_sched_status": "Status",
|
"m365_sched_status": "Status",
|
||||||
"m365_sched_run_now": "▶ Jetzt ausführen",
|
"m365_sched_run_now": "▶ Jetzt ausführen",
|
||||||
"m365_sched_add": "+ Geplante Suche hinzufügen",
|
"m365_sched_add": "+ Geplante Suche hinzufügen",
|
||||||
|
|||||||
@ -753,6 +753,8 @@
|
|||||||
"m365_sched_after_scan": "After scan",
|
"m365_sched_after_scan": "After scan",
|
||||||
"m365_sched_auto_email": "Email report automatically",
|
"m365_sched_auto_email": "Email report automatically",
|
||||||
"m365_sched_auto_retention": "Enforce retention policy",
|
"m365_sched_auto_retention": "Enforce retention policy",
|
||||||
|
"m365_sched_report_only": "Report only",
|
||||||
|
"m365_sched_report_only_hint": "Email the latest scan results without running a new scan. Requires scan results in the database.",
|
||||||
"m365_sched_status": "Status",
|
"m365_sched_status": "Status",
|
||||||
"m365_sched_run_now": "▶ Run now",
|
"m365_sched_run_now": "▶ Run now",
|
||||||
"m365_sched_add": "+ Add scheduled scan",
|
"m365_sched_add": "+ Add scheduled scan",
|
||||||
|
|||||||
@ -43,6 +43,7 @@ _DEFAULT_JOB: dict[str, Any] = {
|
|||||||
"profile_id": "",
|
"profile_id": "",
|
||||||
"auto_email": False,
|
"auto_email": False,
|
||||||
"auto_retention": False,
|
"auto_retention": False,
|
||||||
|
"report_only": False,
|
||||||
"retention_years": None,
|
"retention_years": None,
|
||||||
"fiscal_year_end": None,
|
"fiscal_year_end": None,
|
||||||
}
|
}
|
||||||
@ -270,6 +271,35 @@ class ScanScheduler:
|
|||||||
})
|
})
|
||||||
|
|
||||||
from routes import state
|
from routes import state
|
||||||
|
|
||||||
|
# ── Report-only path: skip scan, email latest DB results ──────────
|
||||||
|
if job_cfg.get("report_only"):
|
||||||
|
if not _m.flagged_items and _m.DB_OK:
|
||||||
|
try:
|
||||||
|
_db_inst = _m._get_db()
|
||||||
|
_db_rows = _db_inst.get_session_items() if _db_inst else []
|
||||||
|
if _db_rows:
|
||||||
|
_m.flagged_items[:] = _db_rows
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not _m.flagged_items:
|
||||||
|
raise RuntimeError(
|
||||||
|
"No scan results available — run a scan first")
|
||||||
|
run["flagged"] = len(_m.flagged_items)
|
||||||
|
run["scanned"] = 0
|
||||||
|
run["status"] = "completed"
|
||||||
|
try:
|
||||||
|
self._send_email_report(job_cfg)
|
||||||
|
run["emailed"] = 1
|
||||||
|
except Exception as _re:
|
||||||
|
run["status"] = "failed"
|
||||||
|
run["error"] = f"Email failed: {_re}"
|
||||||
|
_m.broadcast("scheduler_done", {
|
||||||
|
"flagged": run["flagged"], "scanned": 0,
|
||||||
|
"emailed": run["emailed"], "job_name": job_cfg.get("name", ""),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
# If connector not set, attempt to restore from saved config
|
# If connector not set, attempt to restore from saved config
|
||||||
if not state.connector or not state.connector.is_authenticated():
|
if not state.connector or not state.connector.is_authenticated():
|
||||||
try:
|
try:
|
||||||
@ -455,11 +485,15 @@ class ScanScheduler:
|
|||||||
raise RuntimeError("No email recipients configured")
|
raise RuntimeError("No email recipients configured")
|
||||||
job_name = job_cfg.get("name", "scheduled scan")
|
job_name = job_cfg.get("name", "scheduled scan")
|
||||||
subject = f"GDPR Scanner — {job_name} {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
subject = f"GDPR Scanner — {job_name} {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
||||||
|
if job_cfg.get("report_only"):
|
||||||
|
scan_line = f"Report on latest scan results. {len(_m.flagged_items)} item(s) flagged."
|
||||||
|
else:
|
||||||
|
scan_line = f"Scan completed. {len(_m.flagged_items)} item(s) flagged."
|
||||||
body = (
|
body = (
|
||||||
"<html><body style='font-family:Arial,sans-serif;color:#333;padding:24px'>"
|
"<html><body style='font-family:Arial,sans-serif;color:#333;padding:24px'>"
|
||||||
"<h2 style='color:#1F3864'>🕐 GDPR Scanner — scheduled scan report</h2>"
|
"<h2 style='color:#1F3864'>🕐 GDPR Scanner — scheduled scan report</h2>"
|
||||||
f"<p>Job: <strong>{job_name}</strong></p>"
|
f"<p>Job: <strong>{job_name}</strong></p>"
|
||||||
f"<p>Scan completed. {len(_m.flagged_items)} item(s) flagged.</p>"
|
f"<p>{scan_line}</p>"
|
||||||
f"<p>Report attached: {fname}</p></body></html>")
|
f"<p>Report attached: {fname}</p></body></html>")
|
||||||
from routes.email import _send_email_graph
|
from routes.email import _send_email_graph
|
||||||
from routes import state
|
from routes import state
|
||||||
|
|||||||
@ -51,10 +51,13 @@ function schedRenderJobs() {
|
|||||||
var timeStr = String(j.hour||0).padStart(2,'0') + ':' + String(j.minute||0).padStart(2,'0');
|
var timeStr = String(j.hour||0).padStart(2,'0') + ':' + String(j.minute||0).padStart(2,'0');
|
||||||
var desc = freqLabel + ' ' + timeStr;
|
var desc = freqLabel + ' ' + timeStr;
|
||||||
var chk = j.enabled ? ' checked' : '';
|
var chk = j.enabled ? ' checked' : '';
|
||||||
|
var roBadge = j.report_only
|
||||||
|
? '<span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#E8F4FD;color:#2980B9;border:1px solid #AED6F1;margin-left:4px">' + t('m365_sched_report_only','Report only') + '</span>'
|
||||||
|
: '';
|
||||||
return '<div style="display:flex;align-items:center;gap:6px;padding:5px 6px;border:1px solid var(--border);border-radius:6px;background:var(--surface)">'
|
return '<div style="display:flex;align-items:center;gap:6px;padding:5px 6px;border:1px solid var(--border);border-radius:6px;background:var(--surface)">'
|
||||||
+ '<label class="toggle" style="flex:unset;margin:0"><input type="checkbox"'+chk+' onchange="schedToggleEnabled(\''+sid+'\',this.checked)"><span class="toggle-slider"></span></label>'
|
+ '<label class="toggle" style="flex:unset;margin:0"><input type="checkbox"'+chk+' onchange="schedToggleEnabled(\''+sid+'\',this.checked)"><span class="toggle-slider"></span></label>'
|
||||||
+ '<div style="flex:1;min-width:0">'
|
+ '<div style="flex:1;min-width:0">'
|
||||||
+ '<div style="font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+sname+'</div>'
|
+ '<div style="font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+sname+roBadge+'</div>'
|
||||||
+ '<div id="schedDesc_'+sid+'" style="font-size:10px;color:var(--muted)">'+desc+'</div>'
|
+ '<div id="schedDesc_'+sid+'" style="font-size:10px;color:var(--muted)">'+desc+'</div>'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<button onclick="schedRunJob(\''+sid+'\')" id="schedRunBtn_'+sid+'" style="background:none;border:1px solid var(--border);color:var(--muted);padding:2px 7px;border-radius:4px;font-size:10px;cursor:pointer" title="Run now">▶</button>'
|
+ '<button onclick="schedRunJob(\''+sid+'\')" id="schedRunBtn_'+sid+'" style="background:none;border:1px solid var(--border);color:var(--muted);padding:2px 7px;border-radius:4px;font-size:10px;cursor:pointer" title="Run now">▶</button>'
|
||||||
@ -89,6 +92,8 @@ function schedAddJob() {
|
|||||||
document.getElementById('schedMinute').value = 0;
|
document.getElementById('schedMinute').value = 0;
|
||||||
document.getElementById('schedAutoEmail').checked = false;
|
document.getElementById('schedAutoEmail').checked = false;
|
||||||
document.getElementById('schedAutoRetention').checked = false;
|
document.getElementById('schedAutoRetention').checked = false;
|
||||||
|
document.getElementById('schedReportOnly').checked = false;
|
||||||
|
schedToggleReportOnly();
|
||||||
var titleEl = document.getElementById('schedEditorTitle');
|
var titleEl = document.getElementById('schedEditorTitle');
|
||||||
if (titleEl) titleEl.textContent = t('m365_sched_editor_new', 'New scheduled scan');
|
if (titleEl) titleEl.textContent = t('m365_sched_editor_new', 'New scheduled scan');
|
||||||
schedPopulateProfiles('');
|
schedPopulateProfiles('');
|
||||||
@ -111,6 +116,8 @@ function schedEditJob(id) {
|
|||||||
document.getElementById('schedMinute').value = j.minute != null ? j.minute : 0;
|
document.getElementById('schedMinute').value = j.minute != null ? j.minute : 0;
|
||||||
document.getElementById('schedAutoEmail').checked = !!j.auto_email;
|
document.getElementById('schedAutoEmail').checked = !!j.auto_email;
|
||||||
document.getElementById('schedAutoRetention').checked = !!j.auto_retention;
|
document.getElementById('schedAutoRetention').checked = !!j.auto_retention;
|
||||||
|
document.getElementById('schedReportOnly').checked = !!j.report_only;
|
||||||
|
schedToggleReportOnly();
|
||||||
var titleEl = document.getElementById('schedEditorTitle');
|
var titleEl = document.getElementById('schedEditorTitle');
|
||||||
if (titleEl) titleEl.textContent = t('m365_sched_editor_edit', 'Edit scheduled scan');
|
if (titleEl) titleEl.textContent = t('m365_sched_editor_edit', 'Edit scheduled scan');
|
||||||
schedPopulateProfiles(j.profile_id || '');
|
schedPopulateProfiles(j.profile_id || '');
|
||||||
@ -123,6 +130,19 @@ function schedCancelEdit() {
|
|||||||
document.getElementById('schedJobEditor').style.display = 'none';
|
document.getElementById('schedJobEditor').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function schedToggleReportOnly() {
|
||||||
|
var ro = !!(document.getElementById('schedReportOnly') || {}).checked;
|
||||||
|
var profileRow = document.getElementById('schedProfileRow');
|
||||||
|
var hint = document.getElementById('schedReportOnlyHint');
|
||||||
|
if (profileRow) profileRow.style.opacity = ro ? '0.4' : '';
|
||||||
|
if (hint) hint.style.display = ro ? 'block' : 'none';
|
||||||
|
// Enforce auto_email when switching to report-only
|
||||||
|
if (ro) {
|
||||||
|
var ae = document.getElementById('schedAutoEmail');
|
||||||
|
if (ae) ae.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function schedSaveJob() {
|
function schedSaveJob() {
|
||||||
var name = document.getElementById('schedName').value.trim();
|
var name = document.getElementById('schedName').value.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@ -144,6 +164,7 @@ function schedSaveJob() {
|
|||||||
profile_id: document.getElementById('schedProfile').value,
|
profile_id: document.getElementById('schedProfile').value,
|
||||||
auto_email: document.getElementById('schedAutoEmail').checked,
|
auto_email: document.getElementById('schedAutoEmail').checked,
|
||||||
auto_retention: document.getElementById('schedAutoRetention').checked,
|
auto_retention: document.getElementById('schedAutoRetention').checked,
|
||||||
|
report_only: document.getElementById('schedReportOnly').checked,
|
||||||
};
|
};
|
||||||
var st = document.getElementById('schedSaveStatus');
|
var st = document.getElementById('schedSaveStatus');
|
||||||
st.style.color = 'var(--muted)'; st.textContent = 'Saving...';
|
st.style.color = 'var(--muted)'; st.textContent = 'Saving...';
|
||||||
@ -437,6 +458,7 @@ window.schedSaveJob = schedSaveJob;
|
|||||||
window.schedDeleteJob = schedDeleteJob;
|
window.schedDeleteJob = schedDeleteJob;
|
||||||
window.schedRunJob = schedRunJob;
|
window.schedRunJob = schedRunJob;
|
||||||
window.schedToggleFreqRows = schedToggleFreqRows;
|
window.schedToggleFreqRows = schedToggleFreqRows;
|
||||||
|
window.schedToggleReportOnly = schedToggleReportOnly;
|
||||||
window.schedPopulateProfiles = schedPopulateProfiles;
|
window.schedPopulateProfiles = schedPopulateProfiles;
|
||||||
window.schedLoadHistory = schedLoadHistory;
|
window.schedLoadHistory = schedLoadHistory;
|
||||||
window.schedUpdateSidebarIndicator = schedUpdateSidebarIndicator;
|
window.schedUpdateSidebarIndicator = schedUpdateSidebarIndicator;
|
||||||
|
|||||||
@ -757,12 +757,19 @@ document.addEventListener('DOMContentLoaded', applyI18n);
|
|||||||
<input id="schedMinute" type="number" min="0" max="59" value="0" style="width:50px">
|
<input id="schedMinute" type="number" min="0" max="59" value="0" style="width:50px">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row" id="schedProfileRow">
|
||||||
<label data-i18n="m365_sched_profile">Profile</label>
|
<label data-i18n="m365_sched_profile">Profile</label>
|
||||||
<select id="schedProfile" style="flex:1;height:26px;padding:0 8px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--text);font-size:12px;box-sizing:border-box">
|
<select id="schedProfile" style="flex:1;height:26px;padding:0 8px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--text);font-size:12px;box-sizing:border-box">
|
||||||
<option value="" data-i18n="m365_sched_profile_last">Last saved settings</option>
|
<option value="" data-i18n="m365_sched_profile_last">Last saved settings</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<label data-i18n="m365_sched_report_only">Report only</label>
|
||||||
|
<label class="toggle" style="flex:unset"><input type="checkbox" id="schedReportOnly" onchange="schedToggleReportOnly()"><span class="toggle-slider"></span></label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" id="schedReportOnlyHint" style="display:none">
|
||||||
|
<span style="font-size:10px;color:var(--muted);line-height:1.4" data-i18n="m365_sched_report_only_hint">Email the latest scan results without running a new scan. Requires scan results in the database.</span>
|
||||||
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label data-i18n="m365_sched_auto_email">Email report automatically</label>
|
<label data-i18n="m365_sched_auto_email">Email report automatically</label>
|
||||||
<label class="toggle" style="flex:unset"><input type="checkbox" id="schedAutoEmail"><span class="toggle-slider"></span></label>
|
<label class="toggle" style="flex:unset"><input type="checkbox" id="schedAutoEmail"><span class="toggle-slider"></span></label>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user