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.
This commit is contained in:
@@ -2343,6 +2343,29 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
|
|||||||
animation: busy-spin 0.7s linear infinite;
|
animation: busy-spin 0.7s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes busy-spin { to { transform: rotate(360deg); } }
|
@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; }
|
.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-label { font-size: 11px; font-weight: 500; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
||||||
.slideover-field-value {
|
.slideover-field-value {
|
||||||
|
|||||||
@@ -960,11 +960,31 @@ async function hibpCheckPassword(plaintext) {
|
|||||||
// parallel but with a small concurrency cap so we don't hammer HIBP
|
// parallel but with a small concurrency cap so we don't hammer HIBP
|
||||||
// or trip browser connection limits. Mutates state.hibpResults and
|
// or trip browser connection limits. Mutates state.hibpResults and
|
||||||
// re-renders to show the new badges.
|
// 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() {
|
async function hibpCheckAllEntries() {
|
||||||
if (!state.hibpEnabled || !state.entries.length) return;
|
if (!state.hibpEnabled || !state.entries.length) return;
|
||||||
const CONCURRENCY = 6;
|
const CONCURRENCY = 6;
|
||||||
const queue = state.entries.slice();
|
const queue = state.entries.slice();
|
||||||
const workers = [];
|
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++) {
|
for (let w = 0; w < CONCURRENCY; w++) {
|
||||||
workers.push((async () => {
|
workers.push((async () => {
|
||||||
@@ -978,11 +998,14 @@ async function hibpCheckAllEntries() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Network or decrypt failure: skip silently. Will retry
|
// Network or decrypt failure: skip silently. Will retry
|
||||||
// next time the user opens the vault.
|
// next time the user opens the vault.
|
||||||
|
} finally {
|
||||||
|
hibpProgress(++done, total);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
await Promise.all(workers);
|
await Promise.all(workers);
|
||||||
|
hibpProgress(null);
|
||||||
// The health dashboard reads its "pwned" column from state.hibpResults, and
|
// The health dashboard reads its "pwned" column from state.hibpResults, and
|
||||||
// its cache may well have been computed before these results landed — that
|
// 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
|
// is exactly what happens when HIBP is switched on from the health view
|
||||||
|
|||||||
Reference in New Issue
Block a user