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

+31 -6
View File
@@ -327,6 +327,17 @@ document.addEventListener('DOMContentLoaded', applyI18n);
<button onclick="clearCheckpointAndScan()" style="padding:3px 10px;border-radius:5px;background:none;border:1px solid var(--border);color:var(--muted);cursor:pointer;font-size:12px" data-i18n="m365_btn_start_fresh">Start fresh</button>
</div>
<!-- History mode banner -->
<div id="historyBanner" style="display:none;align-items:center;gap:10px;padding:6px 14px;background:var(--surface);border-bottom:1px solid var(--border);font-size:12px">
<span style="font-size:11px;font-weight:600;color:var(--muted);flex-shrink:0" data-i18n="history_lbl">History</span>
<span id="historyBannerText" style="flex:1;font-size:11px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
<div data-history-wrap style="position:relative;flex-shrink:0">
<button id="historyPickerBtn" type="button" onclick="openHistoryPicker()" style="height:24px;padding:0 10px;background:none;border:1px solid var(--border);color:var(--muted);border-radius:4px;font-size:11px;cursor:pointer" data-i18n="history_btn_sessions">Sessions</button>
<div id="historyDropdown" style="display:none;position:absolute;right:0;top:calc(100% + 4px);background:var(--surface);border:1px solid var(--border);border-radius:6px;z-index:9999;width:300px;max-height:260px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.25)"></div>
</div>
<button id="historyLatestBtn" type="button" onclick="loadHistorySession(null)" style="display:none;height:24px;padding:0 10px;background:none;border:1px solid var(--accent);color:var(--accent);border-radius:4px;font-size:11px;cursor:pointer;flex-shrink:0" data-i18n="history_btn_latest">Latest scan</button>
</div>
<!-- Filter bar — full width, above grid + preview -->
<div class="filter-bar" id="filterBar">
<input type="text" id="filterSearch" data-i18n-placeholder="m365_filter_search" placeholder="Search…" oninput="applyFilters()">
@@ -362,6 +373,7 @@ document.addEventListener('DOMContentLoaded', applyI18n);
<option value="1" data-i18n="m365_filter_special_only">⚠ Art. 9 only</option>
<option value="photo" data-i18n="m365_filter_photo_only">📷 Photos / biometric</option>
</select>
<span id="viewerIdentityBadge" style="display:none;font-size:11px;padding:2px 8px;border-radius:10px;background:var(--muted);color:#fff;font-weight:600;white-space:nowrap;max-width:180px;overflow:hidden;text-overflow:ellipsis"></span>
<select id="filterRole" onchange="applyFilters()" style="width:120px">
<option value="" data-i18n="m365_filter_all_roles">All roles</option>
<option value="staff" data-i18n="m365_filter_staff">Ansatte</option>
@@ -882,14 +894,26 @@ document.addEventListener('DOMContentLoaded', applyI18n);
<div style="font-size:11px;color:var(--muted);margin-bottom:3px" data-i18n="share_label_lbl">Label (optional)</div>
<input id="shareLabel" type="text" data-i18n-placeholder="share_label_placeholder" placeholder="e.g. DPO review 2026" style="width:100%;box-sizing:border-box;font-size:12px;padding:5px 8px;background:var(--surface);border:1px solid var(--border);border-radius:5px;color:var(--text)">
</div>
<div style="width:100px">
<div style="font-size:11px;color:var(--muted);margin-bottom:3px" data-i18n="share_scope_lbl">Role scope</div>
<select id="shareScope" style="width:100%;font-size:12px;padding:5px 6px;background:var(--surface);border:1px solid var(--border);border-radius:5px;color:var(--text)">
<option value="" data-i18n="share_scope_all">All roles</option>
<option value="staff" data-i18n="share_scope_staff">Ansatte</option>
<option value="student" data-i18n="share_scope_student">Elever</option>
<div style="width:120px">
<div style="font-size:11px;color:var(--muted);margin-bottom:3px" data-i18n="share_scope_lbl">Scope</div>
<select id="shareScopeType" onchange="_shareScopeTypeChanged()" style="width:100%;font-size:12px;padding:5px 6px;background:var(--surface);border:1px solid var(--border);border-radius:5px;color:var(--text)">
<option value="" data-i18n="share_scope_all">All</option>
<option value="role" data-i18n="share_scope_type_role">Role</option>
<option value="user" data-i18n="share_scope_type_user">User</option>
</select>
</div>
<div id="shareScopeRoleWrap" style="width:110px;display:none">
<div style="font-size:11px;color:var(--muted);margin-bottom:3px" data-i18n="share_scope_role_lbl">Role</div>
<select id="shareScope" style="width:100%;font-size:12px;padding:5px 6px;background:var(--surface);border:1px solid var(--border);border-radius:5px;color:var(--text)">
<option value="staff" data-i18n="share_scope_staff">Staff</option>
<option value="student" data-i18n="share_scope_student">Students</option>
</select>
</div>
<div id="shareScopeUserWrap" style="flex:1.5;min-width:140px;display:none;position:relative">
<div style="font-size:11px;color:var(--muted);margin-bottom:3px" data-i18n="share_scope_user_lbl">User email</div>
<input id="shareScopeUser" type="text" autocomplete="off" data-i18n-placeholder="share_scope_user_placeholder" placeholder="alice@school.dk" style="width:100%;box-sizing:border-box;font-size:12px;padding:5px 8px;background:var(--surface);border:1px solid var(--border);border-radius:5px;color:var(--text)">
<div id="shareScopeUserDropdown" style="display:none;position:absolute;top:100%;left:0;right:0;margin-top:2px;background:var(--surface);border:1px solid var(--border);border-radius:6px;z-index:9999;max-height:220px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.3)"></div>
</div>
<div style="width:100px">
<div style="font-size:11px;color:var(--muted);margin-bottom:3px" data-i18n="share_expires_in">Expires in</div>
<select id="shareExpiry" style="width:100%;font-size:12px;padding:5px 6px;background:var(--surface);border:1px solid var(--border);border-radius:5px;color:var(--text)">
@@ -1303,5 +1327,6 @@ document.addEventListener('DOMContentLoaded', applyI18n);
<script type="module" src="/static/js/scheduler.js"></script>
<script type="module" src="/static/js/connector.js"></script>
<script type="module" src="/static/js/viewer.js"></script>
<script type="module" src="/static/js/history.js"></script>
</body>
</html>