From c9068a67321691ee3e6d648896fd4ebc3831dcd3 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:58:16 +0100 Subject: [PATCH] feat(hibp): show a progress pill while the breach scan runs Bottom-left, out of the way of the toasts, with a live count. Deliberately not the busy overlay: the scan is network-bound and can take a while, and there is no reason to lock the vault while it runs. --- css/style.css | 23 +++++++++++++++++++++++ js/app.js | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/css/style.css b/css/style.css index 756e78b..b61f641 100644 --- a/css/style.css +++ b/css/style.css @@ -2343,6 +2343,29 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before { animation: busy-spin 0.7s linear infinite; } @keyframes busy-spin { to { transform: rotate(360deg); } } +/* Breach-scan progress. Bottom-left so it never sits under the toasts, which + stack bottom-right. Non-blocking: no backdrop, no pointer capture. */ +.hibp-progress { + position: fixed; + left: 16px; bottom: 16px; + z-index: 150; + display: flex; align-items: center; gap: 8px; + padding: 8px 12px; + border-radius: 8px; + background: var(--bg-elev); + border: 1px solid var(--border); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35); + font-size: 12px; color: var(--text-dim); + pointer-events: none; +} +.hibp-progress-spinner { + width: 14px; height: 14px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: busy-spin 0.7s linear infinite; + flex-shrink: 0; +} .busy-text { font-size: 13px; color: var(--text-dim); text-align: center; } .slideover-field-label { font-size: 11px; font-weight: 500; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; } .slideover-field-value { diff --git a/js/app.js b/js/app.js index 9edfaad..15c07d5 100644 --- a/js/app.js +++ b/js/app.js @@ -960,11 +960,31 @@ async function hibpCheckPassword(plaintext) { // parallel but with a small concurrency cap so we don't hammer HIBP // or trip browser connection limits. Mutates state.hibpResults and // re-renders to show the new badges. +// Progress pill for the breach scan. Created on demand, removed when done — +// pass null to dismiss. Non-blocking by design: the app stays usable. +function hibpProgress(done, total) { + let pill = document.getElementById('hibpProgress'); + if (done === null) { if (pill) pill.remove(); return; } + if (!pill) { + pill = el('div', { class: 'hibp-progress', id: 'hibpProgress' }); + pill.appendChild(el('div', { class: 'hibp-progress-spinner' })); + pill.appendChild(el('span', { class: 'hibp-progress-text' })); + document.body.appendChild(pill); + } + const t = pill.querySelector('.hibp-progress-text'); + if (t) t.textContent = 'Checking breaches… ' + done + '/' + total; +} + async function hibpCheckAllEntries() { if (!state.hibpEnabled || !state.entries.length) return; const CONCURRENCY = 6; const queue = state.entries.slice(); const workers = []; + const total = queue.length; + let done = 0; + // A discreet pill, not the busy overlay: the scan is network-bound and can + // run for a while, and there is no reason to lock the vault meanwhile. + hibpProgress(0, total); for (let w = 0; w < CONCURRENCY; w++) { workers.push((async () => { @@ -978,11 +998,14 @@ async function hibpCheckAllEntries() { } catch (e) { // Network or decrypt failure: skip silently. Will retry // next time the user opens the vault. + } finally { + hibpProgress(++done, total); } } })()); } await Promise.all(workers); + hibpProgress(null); // The health dashboard reads its "pwned" column from state.hibpResults, and // its cache may well have been computed before these results landed — that // is exactly what happens when HIBP is switched on from the health view