Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bb482925f | ||
|
|
f84c8516df | ||
|
|
9fd1aa1f8a |
No files matched your search
@@ -11,6 +11,14 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.7.8] — 2026-06-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Blank results grid after a browser refresh (especially after a server restart)** — restoring the last scan session on page load was one-shot: `_sseWatchdog()` called `loadHistorySession(null)` a single time, guarded by `_initialStatusChecked`. If that attempt was blocked — a completed scan's replayed `scan_phase` event leaves a `_*ScanRunning` flag set, and the `loadHistorySession` guard then bails — nothing retried, because `sse_replay_done` (the other retry path) only fires when the SSE replay buffer is non-empty, and the buffer is empty after a server restart (so refreshing after the in-app self-update reliably showed an empty grid even though the results were in the database). The watchdog now re-attempts the restore on every 4-second poll while nothing is shown and no scan is running, clearing stale running flags first (both scan locks are confirmed free at that point). Additionally, `/api/scan/status` now reports `google_running` separately from `running` (which only ever reflected the M365 + file lock), so a refresh during a live Google scan is detected instead of being treated as idle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.7.7] — 2026-06-15
|
## [1.7.7] — 2026-06-15
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# GDPR Scanner — Brugermanual
|
# GDPR Scanner — Brugermanual
|
||||||
|
|
||||||
Version 1.7.7
|
Version 1.7.8
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -441,8 +441,8 @@ Klik på **🔗**-knappen øverst til højre i topbjælken for at åbne delingsp
|
|||||||
- **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. 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.
|
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. Vælg en **Udløbsdato** — 7 dage, 30 dage, 90 dage, 1 år eller Aldrig.
|
4. Vælg en **Udløbsdato** — 7 dage, 30 dage, 90 dage, 1 år eller Aldrig.
|
||||||
5. Klik på **Opret**. Der genereres et unikt link: `http://host:5100/view?token=…`
|
5. Klik på **Opret**. Formularen ryddes, og det nye link vises øverst i listen **Aktive links** nedenfor, kortvarigt fremhævet.
|
||||||
6. Klik på **Kopiér** for at kopiere linket til udklipsholderen, og send det til gennemgangeren.
|
6. Klik på **Kopiér** i linkets række for at kopiere det 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.
|
||||||
|
|
||||||
@@ -671,4 +671,4 @@ For en typisk skole- eller kommunescanning er omkostningen ubetydelig — Claude
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*GDPR Scanner v1.7.7 — teknisk opsætning og konfiguration: se README.md*
|
*GDPR Scanner v1.7.8 — teknisk opsætning og konfiguration: se README.md*
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# GDPR Scanner — User Manual
|
# GDPR Scanner — User Manual
|
||||||
|
|
||||||
Version 1.7.7
|
Version 1.7.8
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -441,8 +441,8 @@ Click the **🔗** button in the top-right of the top bar to open the Share pane
|
|||||||
- **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. 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.
|
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. Choose an **Expiry** — 7 days, 30 days, 90 days, 1 year, or Never.
|
4. Choose an **Expiry** — 7 days, 30 days, 90 days, 1 year, or Never.
|
||||||
5. Click **Create**. A unique link is generated: `http://host:5100/view?token=…`
|
5. Click **Create**. The form clears and the new link appears at the top of the **Active links** list below, briefly highlighted.
|
||||||
6. Click **Copy** to copy the link to your clipboard, then send it to the reviewer.
|
6. Click **Copy** on that link's row to copy it 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.
|
||||||
|
|
||||||
@@ -671,4 +671,4 @@ For a typical school or municipality scan the cost is negligible — Claude Haik
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*GDPR Scanner v1.7.7 — for technical setup and configuration see README.md*
|
*GDPR Scanner v1.7.8 — for technical setup and configuration see README.md*
|
||||||
+5
-1
@@ -76,8 +76,12 @@ def scan_status():
|
|||||||
acquired = state._scan_lock.acquire(blocking=False)
|
acquired = state._scan_lock.acquire(blocking=False)
|
||||||
if acquired:
|
if acquired:
|
||||||
state._scan_lock.release()
|
state._scan_lock.release()
|
||||||
|
g_acquired = state._google_scan_lock.acquire(blocking=False)
|
||||||
|
if g_acquired:
|
||||||
|
state._google_scan_lock.release()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"running": not acquired,
|
"running": not acquired, # M365 + file scan lock
|
||||||
|
"google_running": not g_acquired, # Google scan lock (separate)
|
||||||
"scan_id": _sse_mod._current_scan_id or None,
|
"scan_id": _sse_mod._current_scan_id or None,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ Never revert to `!!window._googleConnected` / `_fileSources.length > 0` — thos
|
|||||||
## Scan history browser — history.js + results.js
|
## Scan history browser — history.js + results.js
|
||||||
|
|
||||||
- **`S._historyRefScanId`** — `null` = live/SSE mode; positive int = viewing a past session. Set by `loadHistorySession()`; cleared by `exitHistoryMode()`.
|
- **`S._historyRefScanId`** — `null` = live/SSE mode; positive int = viewing a past session. Set by `loadHistorySession()`; cleared by `exitHistoryMode()`.
|
||||||
- **Auto-load on page load** — `results.js` calls `window.loadHistorySession?.(null)` once when the SSE watchdog confirms `!status.running`. `_initialStatusChecked` guard ensures this fires at most once per page load. The `sse_replay_done` handler in `scan.js` retries if `loadHistorySession` bailed due to stale running flags set during replay.
|
- **Auto-load on page load** — `_sseWatchdog()` in `results.js` calls `window.loadHistorySession?.(null)` whenever `/api/scan/status` reports neither `running` (M365 + file lock) nor `google_running` (Google lock) **and** nothing is shown yet (`!S._historyRefScanId && !S.flaggedData.length`). This is **not one-shot** — it retries on every 4s poll until a session is restored, because (a) the replay buffer is empty after a server restart so `sse_replay_done` never fires, and (b) a completed scan's replayed `scan_phase` can leave a running flag set that would otherwise block the load forever. Because both locks are confirmed free, the watchdog clears the stale `_m365/_google/_fileScanRunning` flags before calling. Do not revert to a one-shot `_initialStatusChecked` gate — that reintroduces the "blank grid after refresh/restart" bug. `/api/scan/status` **must** report `google_running` separately; `running` alone misses live Google scans. The `sse_replay_done` handler in `scan.js` still retries for the non-empty-buffer (no-restart) case.
|
||||||
- **History banner** (`#historyBanner`) — shown when `S._historyRefScanId` is set. Do not hide/show from outside `history.js`.
|
- **History banner** (`#historyBanner`) — shown when `S._historyRefScanId` is set. Do not hide/show from outside `history.js`.
|
||||||
- **Session picker** (`#historyDropdown`) — rendered inside `[data-history-wrap]` so the outside-click handler works correctly. Do not move the picker outside this wrapper.
|
- **Session picker** (`#historyDropdown`) — rendered inside `[data-history-wrap]` so the outside-click handler works correctly. Do not move the picker outside this wrapper.
|
||||||
- **Cache invalidation** — `invalidateHistoryCache()` clears `_sessions` and `_latestRefScanId`. All three `*_done` SSE handlers call `window.invalidateHistoryCache?.()`.
|
- **Cache invalidation** — `invalidateHistoryCache()` clears `_sessions` and `_latestRefScanId`. All three `*_done` SSE handlers call `window.invalidateHistoryCache?.()`.
|
||||||
|
|||||||
+18
-9
@@ -736,25 +736,34 @@ function _ensureSSE() {
|
|||||||
|
|
||||||
function _sseWatchdog() {
|
function _sseWatchdog() {
|
||||||
fetch('/api/scan/status').then(function(r) { return r.json(); }).then(function(status) {
|
fetch('/api/scan/status').then(function(r) { return r.json(); }).then(function(status) {
|
||||||
if (status.running) {
|
var anyRunning = status.running || status.google_running;
|
||||||
|
if (anyRunning) {
|
||||||
// A scan is in progress — make sure SSE is connected and progress UI is visible
|
// A scan is in progress — make sure SSE is connected and progress UI is visible
|
||||||
_ensureSSE();
|
_ensureSSE();
|
||||||
if (!S._m365ScanRunning && !S._googleScanRunning && !S._fileScanRunning) {
|
if (status.running && !S._m365ScanRunning && !S._googleScanRunning && !S._fileScanRunning) {
|
||||||
document.getElementById('scanBtn').disabled = true;
|
document.getElementById('scanBtn').disabled = true;
|
||||||
document.getElementById('stopBtn').style.display = 'inline-block';
|
document.getElementById('stopBtn').style.display = 'inline-block';
|
||||||
// /api/scan/status checks the M365 lock — if running=true it's an M365 scan
|
// status.running reflects the M365 + file lock; treat as an M365 reconnect
|
||||||
S._m365ScanRunning = true; _renderProgressSegments();
|
S._m365ScanRunning = true; _renderProgressSegments();
|
||||||
document.getElementById('progressFile').textContent = t('m365_sse_reconnecting', 'Reconnecting to running scan…');
|
document.getElementById('progressFile').textContent = t('m365_sse_reconnecting', 'Reconnecting to running scan…');
|
||||||
log(t('m365_sse_reconnecting', 'Reconnecting to running scan…'));
|
log(t('m365_sse_reconnecting', 'Reconnecting to running scan…'));
|
||||||
}
|
}
|
||||||
|
} else if (!S._historyRefScanId && !(S.flaggedData && S.flaggedData.length)) {
|
||||||
|
// No scan of any kind is running (authoritative, both locks free) and
|
||||||
|
// nothing is shown yet — restore the last saved session from the DB.
|
||||||
|
// Retried on every poll, not one-shot: the initial attempt can be blocked
|
||||||
|
// by running flags that SSE replay of a *completed* scan set but never
|
||||||
|
// cleared, and sse_replay_done only fires for a non-empty buffer (so it
|
||||||
|
// never retries after a server restart clears the replay buffer).
|
||||||
|
// Both locks are confirmed free, so clear any stale flags first.
|
||||||
|
S._m365ScanRunning = false;
|
||||||
|
S._googleScanRunning = false;
|
||||||
|
S._fileScanRunning = false;
|
||||||
|
window.loadHistorySession?.(null);
|
||||||
}
|
}
|
||||||
if (!_initialStatusChecked) {
|
|
||||||
_initialStatusChecked = true;
|
_initialStatusChecked = true;
|
||||||
if (!status.running) window.loadHistorySession?.(null);
|
// Keep polling even when idle — the SSE connection may have died and we
|
||||||
}
|
// need to detect the next scheduled scan (SSE is only opened on demand).
|
||||||
// When no scan is running, we still keep polling — the SSE connection
|
|
||||||
// may have died and we need to detect the *next* scheduled scan.
|
|
||||||
// The SSE itself is only opened/reopened when a scan is detected.
|
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
// Status endpoint unavailable — server might be restarting
|
// Status endpoint unavailable — server might be restarting
|
||||||
console.warn('[SSE] status poll failed:', err);
|
console.warn('[SSE] status poll failed:', err);
|
||||||
|
|||||||
@@ -97,6 +97,22 @@ class TestScanStatus:
|
|||||||
assert "scan_id" in data
|
assert "scan_id" in data
|
||||||
assert data["scan_id"] is None
|
assert data["scan_id"] is None
|
||||||
|
|
||||||
|
def test_idle_reports_google_not_running(self, client):
|
||||||
|
# The refresh/restore path relies on google_running being reported
|
||||||
|
# separately — running alone misses live Google scans.
|
||||||
|
data = client.get("/api/scan/status").get_json()
|
||||||
|
assert data["google_running"] is False
|
||||||
|
|
||||||
|
def test_google_lock_held_reports_google_running(self, client):
|
||||||
|
from routes import state
|
||||||
|
assert state._google_scan_lock.acquire(blocking=False)
|
||||||
|
try:
|
||||||
|
data = client.get("/api/scan/status").get_json()
|
||||||
|
assert data["google_running"] is True
|
||||||
|
assert data["running"] is False # M365/file lock still free
|
||||||
|
finally:
|
||||||
|
state._google_scan_lock.release()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /api/scan/start
|
# /api/scan/start
|
||||||
|
|||||||
Reference in new issue
Block a user