feat: MFA tools, single-instance, tray polish, prefs persistence
Session highlights:
- feat(prefs): DPAPI-backed key/value store (PM.UserPrefs) — fixes
rememberedUsername being lost across reboots due to the random
ephemeral HTTP port changing the localStorage origin every launch.
Bridge cmd://prefs/{get,set} round-trips through Delphi.
- feat(tray): icon visible from startup (NIM_ADD at constructor, not
at first minimize). Tray context menu themed via uxtheme!135
SetPreferredAppMode so it follows the app's dark/light setting.
- feat(single-instance): named mutex + RegisterWindowMessage broadcast.
Second launch posts WM_PMSHOW to HWND_BROADCAST and exits; the
running bridge restores the window from tray. Mutex lives in Local\
namespace so distinct Windows users can still each run one.
- feat(mfa): Authenticator sidebar view (live TOTP codes for every
entry with a secret) + standalone TOTP generator modal (paste
base32 / otpauth:// URI, or generate a random 20-byte secret).
- feat(sidebar): Folders / Tags / Tools sections collapsible with
chevron toggle. Badge counts stay visible when collapsed. State
persisted in settings_json (synced across devices).
- feat(autofill): hotkey when vault is locked now restores the app
and focuses the master password input instead of no-op'ing
silently. Cleaner UX for the common "I hit Ctrl+Shift+L but the
vault was locked" path.
- feat(quick-unlock): when enabled, skip lockVault on Windows lock /
sleep. Rationale: the DPAPI blob already gates access via the
Windows account, so re-locking on top of the OS lock is redundant.
Idle auto-lock still fires (separate opt-in).
- fix(quick-unlock): re-sync state.quickUnlockEnabled from DPAPI
source-of-truth at boot, instead of trusting (now-volatile)
localStorage.
- docs: CLAUDE.md updated with all new modules, bridge commands,
and the port-ephemeral pitfall.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+178
-13
@@ -38,7 +38,11 @@
|
||||
<symbol id="i-log-out" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></symbol>
|
||||
<symbol id="i-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></symbol>
|
||||
<symbol id="i-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></symbol>
|
||||
<symbol id="i-table" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1.4" fill="currentColor"/><circle cx="12" cy="12" r="1.4" fill="currentColor"/><circle cx="12" cy="19" r="1.4" fill="currentColor"/></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol>
|
||||
<symbol id="i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-2 2 2 2-3 3-2-2"/></symbol>
|
||||
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
|
||||
<symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none">
|
||||
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
|
||||
@@ -99,7 +103,16 @@
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Master password</span>
|
||||
<input id="loginPassword" type="password" required>
|
||||
<div class="input-with-action">
|
||||
<input id="loginPassword" type="password" required>
|
||||
<button type="button" class="icon-btn" id="loginPwToggle" title="Show/hide password">
|
||||
<svg><use href="#i-eye"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="remember-row">
|
||||
<input type="checkbox" id="loginRememberUser">
|
||||
<span>Remember username on this device</span>
|
||||
</label>
|
||||
<button id="loginBtn" type="submit" class="btn btn-primary btn-block">
|
||||
<svg><use href="#i-unlock"/></svg>
|
||||
@@ -157,28 +170,51 @@
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section" data-section="folders">
|
||||
<div class="sidebar-section-header">
|
||||
<span>Folders</span>
|
||||
<button class="section-toggle" data-section-toggle="folders">
|
||||
<svg class="section-chevron"><use href="#i-chevron-down"/></svg>
|
||||
<span>Folders</span>
|
||||
<span class="section-count" id="countFolders">0</span>
|
||||
</button>
|
||||
<button class="icon-btn icon-btn-sm" id="addFolderBtn" title="New folder">
|
||||
<svg><use href="#i-plus"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav" id="foldersList"></nav>
|
||||
<nav class="sidebar-nav section-body" id="foldersList"></nav>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-header"><span>Tags</span></div>
|
||||
<nav class="sidebar-nav" id="tagsList"></nav>
|
||||
<div class="sidebar-section" data-section="tags">
|
||||
<div class="sidebar-section-header">
|
||||
<button class="section-toggle" data-section-toggle="tags">
|
||||
<svg class="section-chevron"><use href="#i-chevron-down"/></svg>
|
||||
<span>Tags</span>
|
||||
<span class="section-count" id="countTags">0</span>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav section-body" id="tagsList"></nav>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-header"><span>Tools</span></div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-section" data-section="tools">
|
||||
<div class="sidebar-section-header">
|
||||
<button class="section-toggle" data-section-toggle="tools">
|
||||
<svg class="section-chevron"><use href="#i-chevron-down"/></svg>
|
||||
<span>Tools</span>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav section-body">
|
||||
<button class="nav-item" id="sidebarGenBtn">
|
||||
<svg><use href="#i-dice"/></svg>
|
||||
<span>Generator</span>
|
||||
</button>
|
||||
<button class="nav-item" id="sidebarAuthenticatorBtn">
|
||||
<svg><use href="#i-shield"/></svg>
|
||||
<span>Authenticator</span>
|
||||
</button>
|
||||
<button class="nav-item" id="sidebarTotpToolBtn">
|
||||
<svg><use href="#i-key"/></svg>
|
||||
<span>TOTP generator</span>
|
||||
</button>
|
||||
<button class="nav-item" id="sidebarImportBtn">
|
||||
<svg><use href="#i-log-in"/></svg>
|
||||
<span>Import vault</span>
|
||||
@@ -219,6 +255,9 @@
|
||||
<button class="view-btn" id="viewListBtn" data-view="list" title="List view">
|
||||
<svg><use href="#i-list"/></svg>
|
||||
</button>
|
||||
<button class="view-btn" id="viewTableBtn" data-view="table" title="Table view">
|
||||
<svg><use href="#i-table"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<button class="icon-btn" id="themeBtn" title="Toggle theme">
|
||||
<svg class="theme-icon theme-icon-dark"><use href="#i-sun"/></svg>
|
||||
@@ -233,6 +272,9 @@
|
||||
<span id="userName">user</span>
|
||||
</button>
|
||||
<div class="user-dropdown is-hidden" id="userDropdown">
|
||||
<button class="dropdown-item" id="dropdownSettingsBtn">
|
||||
<svg><use href="#i-settings"/></svg> Settings
|
||||
</button>
|
||||
<button class="dropdown-item" id="lockBtn">
|
||||
<svg><use href="#i-lock"/></svg> Lock vault
|
||||
</button>
|
||||
@@ -287,6 +329,17 @@
|
||||
<option value="light">Light</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span>Sort entries by</span>
|
||||
<select id="settingSort">
|
||||
<option value="name:asc">Name (A → Z)</option>
|
||||
<option value="name:desc">Name (Z → A)</option>
|
||||
<option value="updated:desc">Most recent first</option>
|
||||
<option value="updated:asc">Oldest update first</option>
|
||||
<option value="created:desc">Recently created</option>
|
||||
<option value="created:asc">Oldest created</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span>Compact action menu (⋯)</span>
|
||||
<label class="toggle">
|
||||
@@ -301,6 +354,16 @@
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span>
|
||||
Show site / URL
|
||||
<small class="setting-hint">On cards: shown under the display name when the entry has a custom title. In table view: adds a Site column.</small>
|
||||
</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="settingShowSite">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slideover-field">
|
||||
@@ -333,6 +396,39 @@
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row" id="settingAutofillRow">
|
||||
<span>
|
||||
Autofill with global hotkey
|
||||
<small class="setting-hint">
|
||||
Press your hotkey in any browser to fill the matching
|
||||
login. The vault must be unlocked.
|
||||
</small>
|
||||
</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="settingAutofill">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="settingAutofillHotkeysRow" style="margin-top:8px">
|
||||
<div class="setting-row">
|
||||
<span style="font-size:12px;color:var(--text-dim)">
|
||||
Full fill (username + Tab + password)
|
||||
</span>
|
||||
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillFullCombo">Ctrl+Shift+L</button>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span style="font-size:12px;color:var(--text-dim)">
|
||||
Password only (for step-2 forms / unlock screens)
|
||||
</span>
|
||||
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillPwdCombo">Ctrl+Shift+P</button>
|
||||
</div>
|
||||
<p style="font-size:11px;color:var(--text-faint);margin:6px 0 0;line-height:1.4">
|
||||
Click a button, then press your new combo. Needs Ctrl, Alt or Win + a letter / digit / F-key.
|
||||
</p>
|
||||
<button class="btn btn-ghost btn-sm" id="settingAutofillResetHotkeys" style="margin-top:6px">
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slideover-field">
|
||||
@@ -451,7 +547,7 @@
|
||||
<div class="modal-backdrop" data-close></div>
|
||||
<div class="modal-panel modal-panel-sm">
|
||||
<header class="modal-header">
|
||||
<h3>Change master password</h3>
|
||||
<h3 id="changeMasterTitle">Change master password</h3>
|
||||
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
|
||||
</header>
|
||||
<form id="changeMasterForm" class="modal-body" autocomplete="off">
|
||||
@@ -497,8 +593,12 @@
|
||||
<form id="entryForm" class="modal-body" autocomplete="off">
|
||||
<input type="hidden" id="entryId">
|
||||
<label class="field">
|
||||
<span>Website / App</span>
|
||||
<input id="entrySite" type="text" required placeholder="example.com">
|
||||
<span>Display name <small style="color:var(--text-faint);font-weight:400">— shown on cards, also used for autofill matching</small></span>
|
||||
<input id="entryTitle" type="text" placeholder="GitHub (work account)">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Website / App <small style="color:var(--text-faint);font-weight:400">— hostname or brand, used for autofill matching</small></span>
|
||||
<input id="entrySite" type="text" required placeholder="github.com">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Username / Email</span>
|
||||
@@ -565,6 +665,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- MODAL: Standalone TOTP generator (paste secret → live code) -->
|
||||
<!-- ============================================================ -->
|
||||
<div id="totpToolModal" class="modal is-hidden" role="dialog" aria-modal="true">
|
||||
<div class="modal-backdrop" data-close></div>
|
||||
<div class="modal-panel modal-panel-sm">
|
||||
<header class="modal-header">
|
||||
<h3>TOTP generator</h3>
|
||||
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<p style="margin:0 0 12px;color:var(--text-dim);font-size:13px;line-height:1.5">
|
||||
Paste a base32 secret or an <code>otpauth://</code> URI to
|
||||
generate a code on the fly. Nothing is saved.
|
||||
</p>
|
||||
<label class="field">
|
||||
<span>Secret</span>
|
||||
<input id="totpToolSecret" type="text" autocomplete="off"
|
||||
placeholder="JBSWY3DPEHPK3PXP or otpauth://totp/...">
|
||||
<div style="display:flex;gap:6px;margin-top:6px">
|
||||
<button type="button" class="btn btn-ghost btn-sm" id="totpToolGen" style="flex:1">
|
||||
<svg><use href="#i-dice"/></svg> Generate
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm" id="totpToolCopySecret" style="flex:1">
|
||||
<svg><use href="#i-copy"/></svg> Copy secret
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<div class="totp-panel" style="margin-top:12px">
|
||||
<div class="totp-code" id="totpToolCode">— — — — — —</div>
|
||||
<button class="icon-btn icon-btn-sm" id="totpToolCopy" title="Copy code">
|
||||
<svg><use href="#i-copy"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="totp-bar-wrap"><div class="totp-bar" id="totpToolBar"></div></div>
|
||||
<p id="totpToolError" style="margin:8px 0 0;color:#dc2626;font-size:12px;display:none"></p>
|
||||
</div>
|
||||
<footer class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" data-close>Close</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- CONFIRM / PROMPT MODAL (replaces native confirm/prompt) -->
|
||||
<!-- ============================================================ -->
|
||||
@@ -592,6 +735,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- AUTOFILL PICKER — shown when multiple entries match hotkey -->
|
||||
<!-- ============================================================ -->
|
||||
<div id="autofillPickerModal" class="modal is-hidden" role="dialog" aria-modal="true">
|
||||
<div class="modal-backdrop" data-close></div>
|
||||
<div class="modal-panel modal-panel-sm">
|
||||
<header class="modal-header">
|
||||
<h3 id="autofillPickerTitle">Choose an entry to autofill</h3>
|
||||
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
|
||||
</header>
|
||||
<div class="modal-body" style="padding:8px 0">
|
||||
<p style="margin:0 0 8px 16px;font-size:12px;color:var(--text-dim)">
|
||||
Click an entry to type its credentials into the active window.
|
||||
</p>
|
||||
<div id="autofillPickerList"></div>
|
||||
</div>
|
||||
<footer class="modal-footer">
|
||||
<button class="btn btn-ghost" data-close>Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- COMMAND PALETTE (Cmd+K) — phase 3, shell only for now -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
Reference in New Issue
Block a user