feat: PIN unlock + table column picker + edit-position chooser + UX

- PIN unlock: device-local 4-12 digit shortcut, DPAPI-wrapped vault
  key. Three modes (state.unlockMode): pw / pin / pw+pin. PIN
  derives a wrap key via PBKDF2(pin, salt, 100k) and unwraps the
  stored vault key (mirrors the Quick Unlock blob shape).
  Anti-brute-force: 5 wrong attempts wipes the blob. Setup gated by
  master-pw reauth so an unattended unlocked laptop can't be
  backdoored. Master pw rotation clears the PIN blob (key drift).
  loadServerSettings post-sync demotes pin/both -> pw when the local
  blob is missing, so a wiped device re-syncs the correct mode up.
  New unit PM.PinUnlock.pas + cmd://pin/{store,get,clear,status}.
- Table column picker: ⚙ in topbar (table view only), checkbox menu
  for Site/Username/Folder/Updated. Site also drives showSiteOnCards
  so the existing "Show site / URL" toggle in Settings stays in
  sync. NAME column auto-widths (180px min, content max, +32px
  right padding) so column hugs the next one without truncating.
- Editor position chooser (Appearance setting): Slide-over right /
  left / Centered modal. Scoped to #slideover + #settingsPanel so
  the click-outside / pointer-events logic doesn't accidentally
  trap the modal-style empty viewport.
- Confirm before discarding unsaved edits: state.confirmOnUnsaved
  setting (default ON), prompts on X / Esc / click-outside / switch-
  to-other-entry. Also gates Lock vault / Sign out actions when the
  editor is dirty; auto-lock and system-lock paths bypass to avoid
  blocking on an unattended machine.
- Open-in-browser button added to the actions cell of the table
  view (was card-only).
- Entry templates pass folder customization + template id through
  duplicate / export / import / auto-backup roundtrips.
- Folder color + icon now persisted across export/import: payload.
  folders carries name/color/icon; import creates missing folders
  additively (existing local customisation kept).
- Bulk move-to-folder, batch add-tag, single add-tag now re-ship
  the full entry payload so partial PUTs don't silently wipe
  TOTP / custom_fields / kind / template.
- FireDAC: switched ftString -> ftMemo for icon_b64 / custom_fields
  / TOTP / template params and replaced .AsString with .Value so a
  large (~200 KB) DeepSeek favicon no longer gets truncated at the
  default ANSI 4000-char cap.
- Unicode filenames: attachment INSERT now uses ftWideString +
  .AsWideString so non-ANSI filenames round-trip instead of being
  mangled to "?".
- HandleSetEntryIcon cap raised 256 KB -> 512 KB chars to accept
  base64 data URIs produced by max-raw favicon fetches.
- promptDialog + askReauth support inline `error` line + retry-
  with-count loops on doExport reauth and auto-backup password
  setup (5 attempts cap before bailing).
- Recently used moved from Tools to Vault section in the sidebar.
- Auth screen passkey button hidden (Delphi backend stubs WebAuthn).
- Sensitive cmd://favicon/refresh-style buttons in Settings now
  stopPropagation so the document-level "close panel" handler
  doesn't dismiss Settings mid-async during DOM reparenting.
- TEST_PLAN.md: +PIN unlock section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-06-29 04:41:39 +01:00
parent 1f56a03492
commit b00da43ab0
11 changed files with 1417 additions and 71 deletions
+68 -5
View File
@@ -105,7 +105,7 @@
<span>Username</span>
<input id="loginUsername" type="text" required>
</label>
<label class="field">
<label class="field" id="loginPasswordField">
<span>Master password</span>
<div class="input-with-action">
<input id="loginPassword" type="password" required>
@@ -114,6 +114,15 @@
</button>
</div>
</label>
<label class="field" id="loginPinField" style="display:none">
<span>PIN</span>
<input id="loginPin" type="password" inputmode="numeric"
autocomplete="off" maxlength="12">
</label>
<button type="button" id="loginUseMasterBtn" class="btn btn-link btn-block"
style="font-size:12px;margin-top:-4px;display:none">
Use master password instead
</button>
<label class="remember-row">
<input type="checkbox" id="loginRememberUser">
<span>Remember username on this device</span>
@@ -177,6 +186,10 @@
<span>Notes</span>
<span class="nav-count" id="countNotes">0</span>
</button>
<button class="nav-item" id="sidebarRecentBtn">
<svg><use href="#i-rotate-ccw"/></svg>
<span>Recently used</span>
</button>
</nav>
<div class="sidebar-section" data-section="folders">
@@ -224,10 +237,6 @@
<svg><use href="#i-key"/></svg>
<span>TOTP generator</span>
</button>
<button class="nav-item" id="sidebarRecentBtn">
<svg><use href="#i-rotate-ccw"/></svg>
<span>Recently used</span>
</button>
<button class="nav-item" id="sidebarHealthBtn">
<svg><use href="#i-alert"/></svg>
<span>Vault health</span>
@@ -281,6 +290,12 @@
<svg><use href="#i-table"/></svg>
</button>
</div>
<div class="cols-wrap" id="colsWrap" style="display:none;position:relative">
<button class="icon-btn" id="colsBtn" title="Show / hide columns">
<svg><use href="#i-settings"/></svg>
</button>
<div class="cols-menu is-hidden" id="colsMenu"></div>
</div>
<div class="filters-wrap">
<button class="icon-btn" id="filtersBtn" title="Filters">
<svg><use href="#i-list"/></svg>
@@ -409,6 +424,27 @@
<option value="created:asc">Oldest created</option>
</select>
</div>
<div class="setting-row">
<span>
Editor position
<small class="setting-hint">Where the add / edit panel and Settings appear. Affects both side panels.</small>
</span>
<select id="settingEditorPosition">
<option value="right">Slide-over (right)</option>
<option value="left">Slide-over (left)</option>
<option value="center">Centered modal</option>
</select>
</div>
<div class="setting-row">
<span>
Confirm before closing unsaved edits
<small class="setting-hint">When the entry editor has changes, ask before dismissing it via X, Esc, or click-outside.</small>
</span>
<label class="toggle">
<input type="checkbox" id="settingConfirmUnsaved">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<span>Compact action menu (⋯)</span>
<label class="toggle">
@@ -678,6 +714,33 @@
</button>
</div>
<div class="slideover-field" id="pinUnlockField" style="display:none">
<div class="slideover-field-label">PIN unlock</div>
<p id="pinStatus" style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
No PIN set.
</p>
<p style="font-size:11px;color:var(--text-faint);margin:0 0 8px;line-height:1.4">
Shortcut unlock with a 46 digit PIN. Stored DPAPI-encrypted on
this device. <b>Master password is still required for sensitive
actions</b> (export, change master password, recovery code…).
</p>
<div class="setting-row" style="margin:0 0 8px">
<span>
Unlock method
<small class="setting-hint">Master pw (default), PIN only, or both required.</small>
</span>
<select id="settingUnlockMode">
<option value="pw">Master password</option>
<option value="pin">PIN only</option>
<option value="both">Master password + PIN</option>
</select>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<button class="btn btn-ghost btn-sm" id="pinSetBtn">Set PIN</button>
<button class="btn btn-ghost btn-sm is-danger" id="pinRemoveBtn" style="display:none">Remove PIN</button>
</div>
</div>
<div class="slideover-field">
<div class="slideover-field-label">Quick unlock</div>
<p id="quickUnlockStatus" style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">