navigator.clipboard is undefined in non-secure contexts, so the direct
writeText() call threw synchronously and the execCommand fallback in its
.catch() never ran. _copyText() now feature-detects the API, falls back
to execCommand('copy'), then to a prompt() for manual copying. log.js
reuses the helper; _getShareBaseUrl() caches the LAN-IP lookup so token
Copy buttons stay within the click gesture execCommand requires.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CHANGELOG: cut the 1.7.0 release (dated 2026-06-10); reset Unreleased.
- VERSION: 1.6.28 → 1.7.0.
- Manuals (DA + EN): bump version stamps; correct the redaction section
(cards are now kept/greyed until the next scan, not removed) and add the
same keep-until-next-scan note to the deletion section, including the
partial-failure behaviour.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apply the keep-until-next-scan behaviour to deleteSubjectItems: mark the
deleted items _deleted (using deleted_ids from the response) and keep them
greyed in the grid instead of filtering them out. Also fixes a latent bug
where renderGrid() was called with no argument and threw on files.forEach,
which the surrounding try/catch swallowed as a false "Delete failed" after a
successful erasure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend the keep-until-next-scan behaviour to the bulk delete modal: instead
of removing matched cards on success, mark them _deleted and keep them greyed
with a "🗑 Deleted" badge and hidden buttons. /api/delete_bulk now returns
deleted_ids so the grid marks exactly the items the server actually deleted —
partial failures stay active and re-deletable. Already-handled (_deleted /
_redacted) items are excluded from the bulk-delete match set so they aren't
re-counted or re-processed.
201 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the redact behaviour for the card delete button (🗑): instead of
removing the card on success, mark the item _deleted and keep it in the grid
— greyed via card-resolved, shown with a red "🗑 Deleted" badge, action
buttons hidden so it can't be re-processed. The grid is rebuilt on the next
scan run, clearing the markers. results.js only — no server change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opening the preview panel narrows .grid-area and reflows the auto-fill grid
to fewer columns, moving the clicked card to a new row. The single-frame
scrollIntoView ran while the browser's scroll-anchoring re-adjusted scrollTop
mid-reflow, so the card scrolled out of view. Disable scroll anchoring on
.grid-area (overflow-anchor:none) and defer the scroll by two animation
frames against the settled layout, centring the card (block:'center').
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Redacting a card (✏) previously removed it from the grid and from
S.flaggedData/S.filteredData immediately. Now the item is marked _redacted
and kept: greyed via card-resolved styling, shown with a "✏ Redacted" badge,
and its delete/redact buttons hidden so it can't be re-processed. The grid is
rebuilt on the next scan run, which clears the markers. results.js only — no
server change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The real cause behind the invisible redact/delete buttons: .card lacked
position:relative, so the position:absolute action buttons (delete, redact)
and the bulk-select checkbox anchored to the viewport instead of the card
and were clipped by .card overflow:hidden. They only showed in list view,
where those elements are position:static. Add position:relative to .card so
all three position within each card. Keep the 0.35 baseline opacity on the
redact button for discoverability.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.card-redact-btn had opacity:0 at rest (only opacity:1 on .card:hover), so
the ✏ redact button was completely invisible in the default grid/thumbnail
view — it only showed in list view, which forces opacity:1. Give it the same
0.35 baseline opacity as .card-delete-btn so it's discoverable at rest and
brightens on hover. The button was always rendered in the DOM; this is a
pure visibility fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CHANGELOG: add Unreleased ### Security section covering the stored XSS
in the results grid, the reflected XSS in /api/thumb, and the Claude API
key now being encrypted at rest.
- CLAUDE.md / static/js/CLAUDE.md: add the esc() / _html_esc escaping rule
for scan-derived strings and the onclick-JSON " pattern.
- CLAUDE.md / routes/CLAUDE.md: note that secret config fields use the
machine-keyed Fernet and must be read via a decrypting accessor
(get_claude_api_key()), never config.json directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- results.js: add esc() helper and apply to all scan-derived fields
(name, account_name, folder, source, modified, label, img alt) across
card/list/preview/subject-lookup/related views. Scan-derived strings can
carry attacker-controlled markup (e.g. a OneDrive file named with HTML),
so they must be escaped before innerHTML/attribute embedding. Also escape
the related-docs onclick JSON to match the delete/redact " pattern.
- cpr_detector._placeholder_svg: escape label/name before embedding — served
as image/svg+xml via /api/thumb?name=, so an unescaped value was a
reflected-XSS vector when the URL is opened directly.
- cpr_detector: remove 44-line unreachable duplicate of the face-detection
body left inside _extract_audio_metadata after its return.
- app_config: encrypt claude_api_key at rest with the machine-keyed Fernet
(same as the SMTP password); add get_claude_api_key() for decryption.
Legacy plaintext keys still read and are re-encrypted on next save.
Update readers in document_scanner.py and routes/app_routes.py.
201 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
Immutable audit_log table in the scanner DB records every significant
admin action (profile save/delete, token create/revoke, PIN changes,
source add/update/delete, scheduler job changes, scan start/stop, SMTP
save, dispositions, item delete/redact). GET /api/audit_log exposes
entries newest-first. New Audit Log tab in the Settings modal renders
the table on demand. Settings modal widened 540→640 px and tab labels
set to white-space:nowrap so the six-tab row fits on one line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs in the abort mechanism:
1. POST /api/scan/stop only set state._scan_abort (M365/file abort event)
but never touched state._google_scan_abort. Now sets both.
2. _check_abort() inside _run_google_scan imported gdpr_scanner._scan_abort
(= state._scan_abort, the M365 event) instead of using the module-level
_scan_abort alias (= state._google_scan_abort). This meant the dedicated
/api/google/scan/cancel endpoint — which correctly sets _google_scan_abort
— was silently ignored by the scan loop. Fixed to use the module-level
alias consistently. Also aligned the end-of-scan checkpoint-clear check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Operators can now choose Tesseract language pack(s) per profile via a
sidebar select (#optOcrLang) and profile editor (#peOptOcrLang). Presets:
dan+eng (default), dan, eng, dan+eng+deu, dan+eng+swe, dan+eng+fra. The
ocr_lang option flows from the UI through all three scan engines (M365
files/attachments, Google Drive, Gmail) down to document_scanner.scan_pdf
and scan_image — including the spawned PDF-OCR subprocess worker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflict in CHANGELOG.md: combined the two bug fixes from the
remote branch (stale history results, selected card scroll) with the
local Gmail/Drive preview fix under a single [1.6.26] — 2026-04-29 entry.
Added Preview dispatch documentation to CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking any flagged card that contains CPR hits now shows a "Related documents" section in the preview panel,
listing other items from the same scan session that share at least one CPR number. Items are ordered by number of
shared CPRs; clicking any entry opens it in the preview panel. Works in both live mode and scan history mode.
Implementation
- GDPRDb.get_related_items() — SQL self-join on the existing cpr_index table using the same symmetric 300 s session
window as get_session_items. No new data collection needed.
- GET /api/db/related/<item_id>?ref=N — new endpoint in routes/database.py, consistent with the ?ref convention used
by /api/db/flagged.
- #previewRelated div injected between the metadata block and disposition row in the preview panel.
- _loadRelated(f) in results.js fetches and renders the list; window._openRelated() resolves items from the live
grid or falls back to the API response for history-mode items.
Also
- Added keyword/FTS5 search as a deferred idea in SUGGESTIONS.md
- Updated CHANGELOG.md, README.md, and CLAUDE.md
feat: video/audio metadata scanning, profile rename fix, route tests
- Scan .mp4/.mov/.avi/.mkv and .mp3/.flac/.ogg/.m4a/.wma (+ 7 more)
for GPS coordinates, artist/author, title, comment — metadata only,
no frame or audio analysis. Uses mutagen (added to requirements.txt).
GPS-tagged phone recordings now flag with gps_location like photos.
- Fix _extract_audio_metadata silently returning empty results:
mutagen.File() first positional arg is `filename`, not `fileobj` —
was passing BytesIO as the filename. Fixed to keyword args.
- Fix profile copy rename not reflected in left column until modal
reopen: _pmgmtSaveFullEdit called loadProfiles() but never
_renderProfileMgmt(). Added re-render and active-row highlight.
- Add TestProfileRoutes (10 tests) covering all profile API endpoints
including a rename regression test. Total: 182 tests.
- generate_fixtures.py now produces 6 audio/video fixtures (14–19):
2 MP3, 2 FLAC, 2 MP4 — 4 flagged, 2 negative cases.