feat(entries): encrypt template at rest, guided tour, import fixes, cleanup

Batched session work sharing app.js / index.html / Entries.pas, so it can't
split cleanly without interactive hunk staging.

- feat: encrypt `template` metadata at rest (template_enc/iv, added to
  ENCRYPTED_META_FIELDS). withEncryptedMeta skips an absent template key so
  partial re-ships (add-tag, move-to-folder) don't wipe it via LHasTemplate.
  Cleartext column kept as migration fallback. +3 unit tests.
- feat: first-run guided tour ("How it works") — spotlight + bubble, no GIFs,
  re-launchable from Settings, seen-flag in DPAPI prefs.
- fix(import): preserve original created_at on restore (was stamped to import
  time); restore entry icons on overwrite (PUT ignores icon_b64).
- fix(settings): correct clipboard-privacy copy (already excluded from Win+V);
  PIN text 4-6 -> 4-12; reorder Set-PIN above unlock-method; move tray/startup
  toggles to General; dedicated backup-password button + warning status; tab icons.
- chore: remove dead legacy monolith (app-legacy.js, index-legacy.html,
  style-legacy.css) + unused passkeyBtn stub.
- docs: full-source review (CODE_AUDIT 6b), template + favorite/pinned notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-11 18:32:37 +01:00
parent 92ed153bc0
commit a42e4b205d
15 changed files with 381 additions and 2173 deletions
+14 -2
View File
@@ -1493,7 +1493,7 @@ async function decryptEntryMeta(list) {
// (blanked on write) + ciphertext `f_enc`/`f_iv`. Search/sort/render all run
// client-side on the decrypted in-memory value, so encrypting these is
// transparent. `folder` stays cleartext (server folder-reassign query).
const ENCRYPTED_META_FIELDS = ['username', 'site', 'title', 'tags'];
const ENCRYPTED_META_FIELDS = ['username', 'site', 'title', 'tags', 'template'];
// Choke point for the write path: take an entry body object whose metadata
// fields hold PLAINTEXT, encrypt each into <f>_enc/<f>_iv, and blank the
@@ -1503,6 +1503,12 @@ const ENCRYPTED_META_FIELDS = ['username', 'site', 'title', 'tags'];
async function withEncryptedMeta(obj) {
if (!obj) return obj;
for (const f of ENCRYPTED_META_FIELDS) {
// Partial re-ships (add-tag, move-to-folder, batch ops) omit `template`
// on purpose — the server preserves it when the key is absent
// (LHasTemplate). Synthesising an empty one here would blank the key
// and wipe the stored template. The 4 core fields are always present,
// so this only ever skips `template`.
if (!(f in obj)) continue;
const plain = obj[f] || '';
if (plain) {
const c = await encryptPwd(plain);
@@ -8061,6 +8067,8 @@ async function enterApp() {
// Fire-and-forget periodic backup. Defer a few seconds so the unlock
// path isn't blocked by file I/O + AES-GCM over the full vault.
setTimeout(() => { runAutoBackupIfDue(); }, 5000);
// First-run guided tour (spotlights the headline features once).
maybeStartTour();
}
async function autoPurgeTrashIfNeeded() {
@@ -9123,8 +9131,12 @@ async function init() {
// Autofill picker modal close button
$$('#autofillPickerModal [data-close]').forEach(b =>
b.addEventListener('click', closeAutofillPicker));
$('#startTourBtn').addEventListener('click', () => {
closeSettings();
setTimeout(startTour, 250); // let the panel slide out first
});
$('#openClipboardSettings').addEventListener('click', () => {
toast('Open Windows Settings → System → Clipboard → turn off "Clipboard history"', 'warning');
toast('Copies use the ExcludeClipboardContentFromMonitorProcessing flag, so Windows skips them in Win+V history and cloud sync.');
});
$('#exportBtn').addEventListener('click', doExport);
$('#exportCsvBtn').addEventListener('click', doExportCSV);