${escHtml(c.name)}
@@ -2751,16 +2765,15 @@ const Vault = (() => {
${isFirstTime ? "🔐 MFA enabled — save your backup codes" : "🔐 New backup codes"}
- ${
- isFirstTime
- ? `
+ ${isFirstTime
+ ? `
These codes let you sign in if you lose access to your authenticator app.
Save them now — they will not be shown again.
`
- : `
+ : `
Your previous codes have been invalidated. Save these new codes securely.
`
- }
+ }
${codesHtml}
@@ -3269,14 +3282,14 @@ const Vault = (() => {
const pool = !_activeFilter
? _items
: _activeFilter.type === "itemType"
- ? _items.filter((i) => i.item_type === _activeFilter.value)
- : _activeFilter.type === "folder"
- ? _items.filter((i) => i.folder_id === _activeFilter.value)
- : _activeFilter.type === "tag"
- ? _items.filter((i) =>
- (i.plain?.tags || []).includes(_activeFilter.value),
- )
- : _items;
+ ? _items.filter((i) => i.item_type === _activeFilter.value)
+ : _activeFilter.type === "folder"
+ ? _items.filter((i) => i.folder_id === _activeFilter.value)
+ : _activeFilter.type === "tag"
+ ? _items.filter((i) =>
+ (i.plain?.tags || []).includes(_activeFilter.value),
+ )
+ : _items;
renderItemList(
pool.filter(
(item) =>
@@ -3427,7 +3440,7 @@ const Vault = (() => {
if (
(mode === "add" &&
(document.getElementById("field-type").value || "password") ===
- "password") ||
+ "password") ||
(mode === "edit" && (item?.item_type || "password") === "password")
) {
initPasswordFieldEnhancements();
@@ -3684,7 +3697,7 @@ const Vault = (() => {
"X-CSRFToken": csrfToken(),
},
body: JSON.stringify({ refresh_token: refreshToken }),
- }).catch(() => {});
+ }).catch(() => { });
VaultSession.clear();
SharingSession.clear();
sessionStorage.removeItem("access_token");
@@ -3789,11 +3802,11 @@ const Vault = (() => {
// Auto-clear clipboard after 30 seconds — industry-standard hygiene.
if (_clipboardClearTimer) clearTimeout(_clipboardClearTimer);
_clipboardClearTimer = setTimeout(() => {
- navigator.clipboard.writeText("").catch(() => {});
+ navigator.clipboard.writeText("").catch(() => { });
_clipboardClearTimer = null;
}, 30_000);
})
- .catch(() => {});
+ .catch(() => { });
}
function escHtml(str) {
diff --git a/extension/popup/popup.css b/extension/popup/popup.css
index b610a29..12691a9 100644
--- a/extension/popup/popup.css
+++ b/extension/popup/popup.css
@@ -932,6 +932,7 @@ body {
background: #e0f2fe;
color: #0369a1;
}
+
/* ── Three-dot flyout menu ───────────────────────────────────────────────── */
.pk-flyout {
@@ -940,7 +941,7 @@ body {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
- box-shadow: 0 4px 16px rgba(0,0,0,0.13);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.13);
min-width: 150px;
padding: 4px 0;
display: flex;
@@ -972,9 +973,20 @@ body {
color: #6b7280;
}
+.pk-flyout-info {
+ padding: 6px 12px 7px;
+ font-size: 11px;
+ color: #9ca3af;
+ border-top: 1px solid #f3f4f6;
+ margin-top: 2px;
+ user-select: none;
+}
+
/* ── Collapsible folder groups ────────────────────────────────────────────── */
-.pk-group { margin-bottom: 2px; }
+.pk-group {
+ margin-bottom: 2px;
+}
.pk-group-header {
display: flex;
@@ -988,7 +1000,9 @@ body {
transition: background 0.1s;
}
-.pk-group-header:hover { background: #f0f4ff; }
+.pk-group-header:hover {
+ background: #f0f4ff;
+}
.pk-group-name {
font-size: 12px;
@@ -1091,4 +1105,4 @@ body {
.reprompt-actions .btn-primary,
.reprompt-actions .btn-ghost {
margin-top: 0;
-}
+}
\ No newline at end of file
diff --git a/extension/popup/popup.js b/extension/popup/popup.js
index 23a2c48..ed9411c 100644
--- a/extension/popup/popup.js
+++ b/extension/popup/popup.js
@@ -69,10 +69,10 @@ function escHtml(str) {
// Auto-clear clipboard 30 s after a sensitive copy.
let _clipTimer = null;
function _copyWithAutoClear(text) {
- navigator.clipboard.writeText(text).catch(() => {});
+ navigator.clipboard.writeText(text).catch(() => { });
if (_clipTimer) clearTimeout(_clipTimer);
_clipTimer = setTimeout(() => {
- navigator.clipboard.writeText("").catch(() => {});
+ navigator.clipboard.writeText("").catch(() => { });
_clipTimer = null;
}, 30_000);
}
@@ -186,7 +186,7 @@ function siteLabel(item) {
if (item.plain?.url) {
try {
return new URL(item.plain.url).hostname.replace(/^www\./, "");
- } catch {}
+ } catch { }
}
return item.name;
}
@@ -441,7 +441,7 @@ async function completeLogin(data, masterPassword) {
refresh_token: data.refresh_token,
enc_key_salt: data.enc_key_salt,
})
- .catch(() => {});
+ .catch(() => { });
showView("vault");
await checkPendingSave();
@@ -502,9 +502,9 @@ async function signOut() {
Authorization: `Bearer ${access_token}`,
},
body: JSON.stringify({ refresh_token }),
- }).catch(() => {});
+ }).catch(() => { });
}
- } catch {}
+ } catch { }
await chrome.storage.session.clear();
await chrome.storage.local.remove(["refresh_token", "enc_key_salt"]);
// vault_items_cs is now in session storage — cleared by the session.clear() call above.
@@ -616,7 +616,7 @@ async function fetchAndDecryptVault() {
type: "VAULT_UPDATED",
vault_items: itemsForContentScript,
})
- .catch(() => {});
+ .catch(() => { });
// Run health checks in the background — sync metrics first, then HIBP.
// Results are sent to the background SW via HEALTH_UPDATE so the toolbar
@@ -645,7 +645,7 @@ async function _runPopupHealthCheck() {
if (!pwItems.length) {
chrome.runtime
.sendMessage({ type: "HEALTH_UPDATE", breached: 0, weak: 0, reused: 0 })
- .catch(() => {});
+ .catch(() => { });
return;
}
@@ -676,7 +676,7 @@ async function _runPopupHealthCheck() {
weak: weak.length,
reused: reused.length,
})
- .catch(() => {});
+ .catch(() => { });
// HIBP — k-anonymity, parallel.
const hibpResults = await Promise.all(
@@ -695,7 +695,7 @@ async function _runPopupHealthCheck() {
weak: weak.length,
reused: reused.length,
})
- .catch(() => {});
+ .catch(() => { });
} catch (err) {
console.error("[PassKeeper] Popup health check failed:", err);
}
@@ -1053,7 +1053,7 @@ function renderList() {
password: item.plain.password || "",
autologin: !!item.plain.autologin,
})
- .catch(() => {});
+ .catch(() => { });
}
window.close();
}),
@@ -1093,44 +1093,64 @@ function renderList() {
const menuItems = [
item.plain?.url
? {
- label: "Open URL",
- icon: '
',
- action: async () => {
- chrome.tabs.create({ url: item.plain.url });
- flyout.remove();
- },
- }
+ label: "Open URL",
+ icon: '
',
+ action: async () => {
+ chrome.tabs.create({ url: item.plain.url });
+ flyout.remove();
+ },
+ }
: null,
item.plain?.username
? {
- label: "Copy username",
- icon: '
',
- action: async () => {
- if (item.plain?.reprompt) {
- const ok = await _repromptMasterPassword();
- if (!ok) return;
- }
- _copyWithAutoClear(item.plain.username);
- flyout.remove();
- },
- }
+ label: "Copy username",
+ icon: '
',
+ action: async () => {
+ if (item.plain?.reprompt) {
+ const ok = await _repromptMasterPassword();
+ if (!ok) return;
+ }
+ _copyWithAutoClear(item.plain.username);
+ flyout.remove();
+ },
+ }
: null,
item.plain?.password
? {
- label: "Copy password",
- icon: '
',
- action: async () => {
- if (item.plain?.reprompt) {
- const ok = await _repromptMasterPassword();
- if (!ok) return;
- }
- _copyWithAutoClear(item.plain.password);
- flyout.remove();
- },
- }
+ label: "Copy password",
+ icon: '
',
+ action: async () => {
+ if (item.plain?.reprompt) {
+ const ok = await _repromptMasterPassword();
+ if (!ok) return;
+ }
+ _copyWithAutoClear(item.plain.password);
+ flyout.remove();
+ },
+ }
: null,
].filter(Boolean);
+ // Add a non-action info row showing when the password was last changed.
+ // Only shown for password items that have password_changed_at set.
+ if (item.item_type === "password" && item.plain?.password_changed_at) {
+ const daysAgo = Math.floor(
+ (Date.now() - new Date(item.plain.password_changed_at).getTime()) /
+ 86400000,
+ );
+ const ageText =
+ daysAgo === 0
+ ? "Changed today"
+ : daysAgo === 1
+ ? "Changed yesterday"
+ : `Changed ${daysAgo} days ago`;
+ const infoRow = document.createElement("div");
+ infoRow.className = "pk-flyout-info";
+ infoRow.textContent = ageText;
+ // Append after action rows are added.
+ flyout._ageInfoRow = infoRow;
+ }
+
menuItems.forEach((mi) => {
const row = document.createElement("button");
row.className = "pk-flyout-item";
@@ -1142,6 +1162,11 @@ function renderList() {
flyout.appendChild(row);
});
+ // Append age info row if present.
+ if (flyout._ageInfoRow) {
+ flyout.appendChild(flyout._ageInfoRow);
+ }
+
// Position flyout using fixed coords so it escapes vault-list overflow clipping.
const appEl = document.getElementById("app");
appEl.appendChild(flyout);
@@ -1246,7 +1271,7 @@ async function checkPendingSave() {
function _clearPendingSave() {
$("save-prompt-overlay").classList.add("hidden");
chrome.storage.local.remove("pending_save");
- chrome.runtime.sendMessage({ type: "CLEAR_SAVE_BADGE" }).catch(() => {});
+ chrome.runtime.sendMessage({ type: "CLEAR_SAVE_BADGE" }).catch(() => { });
}
$("btn-save-yes").onclick = async () => {
@@ -1484,7 +1509,7 @@ async function initAddView() {
: new URL(tab.url).hostname.replace(/^www\./, "");
}
}
- } catch (e) {}
+ } catch (e) { }
// Load folders into the add-folder select.
const addFolderSel = $("add-folder");
@@ -1500,7 +1525,7 @@ async function initAddView() {
addFolderSel.appendChild(opt);
});
}
- } catch (e) {}
+ } catch (e) { }
// Wire one-time event listeners.
if (!_addViewInitialised) {
@@ -1635,7 +1660,7 @@ async function initAccountView() {
// Tell background to re-apply the new interval.
chrome.runtime
.sendMessage({ type: "SET_IDLE_TIMEOUT", seconds: val })
- .catch(() => {});
+ .catch(() => { });
// Brief "Saved" confirmation.
const saved = $("acct-idle-saved");
saved.classList.remove("hidden");
@@ -1656,7 +1681,7 @@ async function init() {
currentWindow: true,
});
_currentUrl = tab?.url || "";
- } catch {}
+ } catch { }
// Set vault link
$("btn-open-vault").href = VAULT_URL;
@@ -1672,7 +1697,7 @@ async function init() {
showView("generator");
initGenerator();
// Still load vault data in background so the Vault tab is ready.
- fetchAndDecryptVault().then(() => {});
+ fetchAndDecryptVault().then(() => { });
// Wire event listeners below, then return early from vault-specific setup.
} else {
showView("vault");
@@ -1752,7 +1777,7 @@ async function init() {
// browser is open. We ping it every 20 s; the ping itself is a no-op but
// prevents the SW from being killed between popup openings.
const _keepalive = setInterval(() => {
- chrome.runtime.sendMessage({ type: "KEEPALIVE" }).catch(() => {});
+ chrome.runtime.sendMessage({ type: "KEEPALIVE" }).catch(() => { });
}, 20_000);
// Search