feat: secure notes + password history + custom fields + quick-win bundle

Big feature trio
- Secure notes (kind='login'|'note') reusing the encrypted_password+iv
  pipeline for the body. New sidebar entry, slideover variant (title +
  multiline body), distinct card / table-view rendering, badge in name
  column, copy-content button replacing the password copy on note rows.
- Password history: entries_password_history table keeps up to 20 prior
  ciphertexts per entry. HandleUpdateEntry pushes the pre-update
  encrypted_password into history ONLY when it actually differs from
  the incoming one (JS reuses originalEncrypted bit-for-bit when the
  plaintext is unchanged — avoids spamming history on title/folder edits).
  GET /entries/{id}/history endpoint. Slideover modal lists versions
  with mask/reveal/copy/revert. Master-pw rotation wipes history (old
  ciphertext can't be decrypted with the new key).
- Custom fields: per-entry encrypted JSON array of {label, value,
  is_secret}. Same crypto pipeline as the password. Slideover row UI
  with label/value inputs, secret toggle (eye), copy, delete. Re-
  encryption flows through bulk-import, change-master-password, and
  duplicate.

Quick wins
- Cheatsheet overlay (press '?' or topbar button or Ctrl+K). Lists all
  hotkeys + global / tray / card actions. SVG icons inline so the
  cheatsheet matches the actual app glyphs (no emoji mismatch).
- Open URL button on entry cards: ShellExecute via cmd://app/open-url,
  http(s) only, validates entry.site looks like a real hostname.
- Trash auto-purge: setting "Empty trash after N days" (never/7/30/90).
  DELETE /entries/trash/old?days=N called at every unlock.

Favicon strategy
- Subdomains (chat.deepseek.com, app.X.com…) now try the SLD first
  (deepseek.com.ico) before the full host. DDG often returns a generic
  placeholder for subdomains that passes the byte threshold; the SLD-first
  switch surfaces the real brand icon.
- Cap bumped 64 KB → 256 KB on all three sides (Delphi fetch, server
  endpoint, JS upload). DDG sometimes serves the full-res asset.

UX polish
- Click-outside-slideover: stopPropagation everywhere it bites. Custom
  fields buttons (add / delete / secret toggle / copy / eye) all stop
  the click bubble so the document-level "close on outside click" handler
  doesn't fire when rerender() detaches the target from the DOM.
- Native search-cancel button restyled: cyan accent X via mask-image,
  cursor: pointer, breathing room before the Ctrl+K kbd chip.
- Password history modal: scrollable body, multiline wrapped passwords,
  hover border highlight.
- Cheatsheet panel widened (560 → 720 px) so the descriptions no longer
  ellipsis-clip.
- "+ New" topbar splits into a small dropdown: New login / New note.
- Notes show a "note" badge in table-view name column, italic
  "Encrypted note" placeholder in the username column.

Internals
- duplicateEntry copies kind + custom_fields too (one-line forgotten
  earlier).
- entries_password_history dropped on master-pw rotation — the old
  ciphertexts are unrecoverable with the new key.
- bulk-import re-encryption path includes custom_fields.

CLAUDE.md
- "Entry payload — call sites à toucher ensemble" lists the 6 spots
  to update when adding a new (en)crypted field. Notes the historical
  miss of kind in duplicateEntry and custom_fields in the rotation +
  duplicate.

Repo hygiene
- .gitattributes forces CRLF on Delphi sources (RAD Studio refuses LF).
  text=auto for web frontend / docs, binary for .res / .exe / images.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:17:19 +01:00
parent 39406d712e
commit 63fac5b3b7
11 changed files with 1487 additions and 113 deletions
+80 -4
View File
@@ -168,6 +168,11 @@
<span>Favorites</span>
<span class="nav-count" id="countFav">0</span>
</button>
<button class="nav-item" data-view="notes">
<svg><use href="#i-edit"/></svg>
<span>Notes</span>
<span class="nav-count" id="countNotes">0</span>
</button>
</nav>
<div class="sidebar-section" data-section="folders">
@@ -263,14 +268,33 @@
<svg><use href="#i-table"/></svg>
</button>
</div>
<button class="icon-btn" id="cheatsheetBtn" title="Keyboard shortcuts (?)">
<span style="font-weight:700;font-size:14px">?</span>
</button>
<button class="icon-btn" id="themeBtn" title="Toggle theme">
<svg class="theme-icon theme-icon-dark"><use href="#i-sun"/></svg>
<svg class="theme-icon theme-icon-light"><use href="#i-moon"/></svg>
</button>
<button class="btn btn-primary btn-sm" id="newEntryBtn">
<svg><use href="#i-plus"/></svg>
New
</button>
<div class="new-entry-wrap">
<button class="btn btn-primary btn-sm" id="newEntryBtn">
<svg><use href="#i-plus"/></svg>
New
</button>
<button class="btn btn-primary btn-sm new-entry-caret" id="newEntryCaretBtn"
title="Choose entry type">
<svg><use href="#i-chevron-down"/></svg>
</button>
<div class="new-entry-menu is-hidden" id="newEntryMenu">
<button class="dropdown-item" data-new-kind="login">
<svg><use href="#i-key"/></svg>
<span>New login</span>
</button>
<button class="dropdown-item" data-new-kind="note">
<svg><use href="#i-edit"/></svg>
<span>New note</span>
</button>
</div>
</div>
<div class="user-menu">
<button class="user-chip" id="userChip">
<span id="userName">user</span>
@@ -416,6 +440,22 @@
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<span>
Auto-purge trash after
<small class="setting-hint">
Permanently delete entries that have been in
the trash for longer than this. Runs at every
unlock.
</small>
</span>
<select id="settingTrashPurge">
<option value="0">Never</option>
<option value="7">7 days</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
</select>
</div>
<div class="setting-row">
<span>
Check passwords against breach database (HIBP)
@@ -723,6 +763,42 @@
</div>
</div>
<!-- ============================================================ -->
<!-- MODAL: Password history — list previous versions + revert -->
<!-- ============================================================ -->
<div id="historyModal" 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="historyTitle">Previous versions</h3>
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
</header>
<div class="modal-body" id="historyBody"></div>
<footer class="modal-footer">
<span style="font-size:11px;color:var(--text-faint);line-height:1.4">
Up to 20 versions kept. Master-password change clears the history.
</span>
</footer>
</div>
</div>
<!-- ============================================================ -->
<!-- CHEATSHEET overlay — press '?' to discover hotkeys -->
<!-- ============================================================ -->
<div id="cheatsheetModal" class="modal is-hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop" data-close></div>
<div class="modal-panel cheatsheet-panel">
<header class="modal-header">
<h3>Keyboard shortcuts</h3>
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
</header>
<div class="modal-body cheatsheet-body" id="cheatsheetBody"></div>
<footer class="modal-footer">
<span style="font-size:11px;color:var(--text-faint)">Press <kbd>?</kbd> anytime to reopen.</span>
</footer>
</div>
</div>
<!-- ============================================================ -->
<!-- MODAL: Quick search (tray menu → fast password copy) -->
<!-- ============================================================ -->