Commit Graph
215 Commits
Author SHA1 Message Date
Frederic CabassutandClaude Fable 5.1 082b8632c6 fix: render empty task list items as checkboxes in note previews
The task-list markdown rule only matched a marker followed by a space
("[ ] text"). markdown-it trims trailing whitespace from inline content,
so an item with no text arrived as exactly "[ ]" and rendered as literal
brackets in the card preview, while the editor showed a checkbox.

Match the marker followed by a space or end-of-content instead, and strip
only what was matched so "[ ]x" glued to text stays literal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 19:13:53 +02:00
Frederic Cabassut 12ff6d9c4d chore(deps): update markdown-it to v15 for quadratic-complexity fixes
v15.0.1 fixes quadratic-complexity bugs in the linkify inline rule and
fuzzy-link replacement - relevant here since this app renders notes
with linkify: true. Also drops @types/markdown-it, which v15 makes
redundant by bundling its own TypeScript declarations (conflicting
otherwise), and adapts two call sites to the now-more-precise
attrGet() return type (string | number | null vs the community types'
string | null).
2026-09-02 09:34:03 +02:00
Frederic Cabassut 27c3e7dccd fix(editor): render the table-with-list HTML fallback safely instead of escaping it
The table markdown fallback (previous commit) embeds raw HTML in saved
markdown when a cell holds a bullet list, since GFM can't express block
content in a table cell. But the read-only renderer (note-card preview,
public share page) ran with html:false, so that HTML rendered as
escaped literal text instead of a table - moving the bug rather than
fixing it.

Enable html mode and sanitize the rendered output with sanitize-html.
Sanitizing matters beyond this one fallback: note content isn't only
ever written through the editor - the MCP server and notes API accept
a raw markdown string directly, bypassing the editor's schema, so any
HTML this renders has to be treated as untrusted regardless of source.

Also make the table's HTML fallback respect the editor's own `html`
option (falls back to a `[table]` placeholder when disabled), matching
every other node type's markdown storage in tiptap-markdown.

Found via a 3-agent adversarial review of the previous fix.
2026-09-01 12:00:36 +02:00
Frederic Cabassut 64f96b21a3 fix(editor): preserve bullet lists inside table cells on save/reopen
tiptap-markdown's bundled table serializer only rejects cells with
multiple children or spans, not a single non-paragraph block child. A
cell holding just a bullet list slipped through and got fed to
renderInline(), which is only valid for textblocks, corrupting the row
on save. Now such cells fall back to serializing the whole table as
raw HTML, same as tiptap-markdown does for other unsupported layouts.

Fixes #83
2026-09-01 12:00:36 +02:00
Frederic Cabassut e4606cfc70 feat(auth): enable password login on OAuth-only accounts, with disable control
Admins can now reset the password of an OAuth-only user, which creates
their credentials and enables password login; the new 'Disable password
login' row action removes those credentials again. Resetting a password
revokes all existing sessions.

- add users.password_login_enabled column (migration 0005)
- admin reset: set password, enable login, revoke sessions
- disable action: clear password, disable login (only on OAuth-linked rows)
- account endpoint: refuse password updates on OAuth-linked users
- e2e: wipe the test DB before the webServer starts (Playwright starts
  the server before globalSetup, so wiping there orphaned the DB file)
- row-actions strip test: five square actions, wrap on phone widths
2026-08-19 13:18:56 +02:00
Frederic CabassutandClaude Opus 5 51dd844f96 feat(wu-001): add password generator and reset-eligibility utilities
Groundwork for issue #77 (admin cannot reset other users' passwords).
Two pure, colocated-tested modules shared by the API route and the
settings UI that follow.

password.ts — generatePassword() over a charset pinned at exactly 64
characters. 64 is a power of two, so nanoid's customAlphabet takes its
exactly-uniform masking path (256 % 64 === 0) rather than the rejection
branch; 120 bits of entropy at the default length of 20. Visually
ambiguous characters (0 O o 1 l I) are excluded so a password survives
being read aloud or retyped. Class coverage is guaranteed by discarding
and regenerating a whole password that is missing a group, which stays
uniform over exactly the set of valid passwords — rather than forcing
characters into positions and shuffling, which would need its own
unbiased index source.

auth-methods.ts — canResetPassword() and passwordResetBlockedReason(),
the single source of truth for eligibility so the endpoint and the
component cannot drift. Password login is gated on authProvider
(verifyPassword matches only rows where it equals 'password'), so
writing a hash for any other provider stores a credential that can
never be used to sign in.

The reason lookup is a Map, not an object literal: an object inherits
Object.prototype, so a lookup for 'toString' or '__proto__' resolves to
an inherited member and the nullish fallback never fires — returning a
function where the signature promises string | null. A Map has no such
keys, so totality holds by construction instead of by a guard a later
refactor could drop.

DoD items verified:
- [x] 1. generatePassword() returns 20 chars; explicit length honoured
- [x] 2. always >=1 lowercase, uppercase, digit and symbol
- [x] 3. never contains 0 O o 1 l I
- [x] 4. randomness from nanoid customAlphabet; no Math.random
- [x] 5. throws for length < 8 (the server minimum)
- [x] 6. retry bounded at 100 with an unconditional throw (inspection)
- [x] 7. canResetPassword true only for 'password'
- [x] 8. totality: every blocked input yields a non-empty string
- [x] 9. exact reason copy per provider; null when allowed
- [x] 10. colocated tests pass

Verified independently by the orchestrator: pnpm check 0 errors /
14 pre-existing warnings; pnpm test:unit 306 passed / 26 files (from
288 / 24); coverage 45.52 lines, 45.52 statements, 54.01 functions,
84.87 branches — every metric above both its .coverage-thresholds.json
floor and the previous baseline, new files at 100%.

Reviewed-by: adversarial-review (PASS, second reviewer; first found a
prototype-chain totality violation, since fixed and re-reviewed fresh)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbqJcFofZSbhbGwUwZP3wU
2026-08-18 12:31:12 +02:00
Frederic CabassutandClaude Sonnet 5 3eb8d2eeab fix: close note-switching race conditions and restore offline-first note open
Three issues found in review, all around switching to a different note
while the editor is already open (via a note-link or backlink chip):

- TiptapEditor's onTransaction/onUpdate had no guard against writing
  state after the component is torn down. Removing a focused editor's
  DOM node fires a native blur, which ProseMirror turns into one last
  transaction synchronously during that same teardown pass, crashing
  with state_unsafe_mutation. Deferred the onTransaction write to a
  microtask (checked against a destroyed flag) so it lands after
  Svelte's current pass finishes; guarded onUpdate synchronously.

- openEditor/openNoteById in NotesView.svelte awaited a network fetch
  before ever showing the editor, for every note open (not just
  link-driven navigation) — a regression for an offline-first app, and
  the source of a real race: two rapid note-link clicks left two
  fetches in flight with no ordering guarantee, so whichever resolved
  last won, not whichever was clicked last. Reverted to opening
  instantly from local/IndexedDB data (with a request-id guard against
  the residual race), and instead fetch backlinks lazily in
  NoteEditor.svelte only if not already present — mirroring the
  existing attachments-fetch pattern already in that file. Also
  applied the same post-teardown guard to that pre-existing attachments
  effect, since remounting on note-switch newly exposes it to the same
  class of race.

- FormattingToolbar's closeDropdowns() cleared search results but never
  cancelled the pending debounce timer, so a stale query could
  repopulate results after the dropdown had already closed. Added the
  missing clearTimeout, plus a try/catch around the search fetch for
  consistency with the pattern used elsewhere in the app.

Verified: pnpm check (0 errors), full unit suite (316/316), and the
existing e2e suite including all note-linking scenarios (19/19) all
pass against these changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:51:08 +02:00
Arkaprava Gaine bbfde5bac0 custom utility component of tiptap to solve the thing : the 2026-08-05 10:51:08 +02:00
Arkaprava Gaine f33a4037b1 utils and markdown changes needed for crumb:// linking to work 2026-08-05 10:51:08 +02:00
Arkaprava Gaine e6d1f18311 adding UI for note-linking 2026-08-05 10:51:08 +02:00
Arkaprava Gaine 1eebd009ac server functions and service wiring on save and attach backlinks 2026-08-05 10:51:08 +02:00
Arkaprava Gaine 1ca2ba5b79 extrcation notes utility for parsing note links 2026-08-05 10:51:08 +02:00
Arkaprava Gaine 331c56981d add notelinks tabel for note to note references 2026-08-05 10:51:08 +02:00
Frederic Cabassut c640868957 Revert "feat: add extractNoteLinks utility for parsing note-link references"
This reverts commit 39e247a46c.
2026-08-05 10:32:26 +02:00
Frederic Cabassut c341fe2b86 feat: add extractNoteLinks utility for parsing note-link references 2026-08-03 15:46:19 +02:00
Frederic CabassutandClaude Sonnet 5 d4ed358ba3 fix(security): patch dependency CVEs and close SSRF/XSS gaps in MCP image upload
Bumps deps to clear all 69 pnpm audit findings (nodemailer 8->9 for the
raw-option SSRF/arbitrary-file-read CVE; svelte, hono/express/qs chain,
linkify-it, and the rest resolved within their existing semver ranges
via a lockfile refresh). Caps the previously-unbounded cookie override
at <1 so it can't silently jump to the API-incompatible 2.x line again.

Hardens the MCP upload_image tool, which fetched an arbitrary caller-
supplied URL and stored whatever content-type came back with no checks:
- validates every resolved DNS record (not just the first) against
  private/loopback/link-local/multicast ranges, then pins the actual
  connection to the validated address so a second, unvalidated lookup
  at connect time can't be used for DNS-rebinding SSRF
- rejects non-image content-types and enforces the same 10MB cap as
  the manual upload endpoint
- adds X-Content-Type-Options: nosniff to both attachment-serving
  routes as defense in depth

Also validates the admin role field server-side (previously trusted
the client-supplied string), and fixes two incidental regressions
surfaced by the dependency bump: StarterKit now bundles its own
Link/Underline extensions (disabled to avoid duplicate registration),
and FormattingToolbar could call methods on an already-destroyed
editor instance during the markdown-mode toggle race.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 13:43:25 +02:00
Frederic Cabassut a46387172c fix(theme): restore hover-wash contrast and correct doc claims from dark palette review
Separates --hover-wash from --border so the softened dark-mode border grey
no longer collapses hover/active tint washes to near-invisibility (measured
5% wash on --bg-surface: 4.7 -> 17.0 RGB distance). Light mode is unaffected
since --hover-wash falls back to var(--border) in :root.

Also fixes stale/incorrect claims surfaced by the final review: the
checkbox rule and missing CSS variables in CLAUDE.md, the "warm parchment"
description of dark mode in FEATURES.md, the chip contrast claim in the
palette design doc (chips are 3:1 UI, not 4:1 text), and adds a measured
lightness-floor test guarding against chips regressing to card lightness.
2026-07-31 12:03:39 +02:00
Frederic CabassutandClaude Opus 5 e8bef26b4f fix(theme): sync browser theme-colour with the new dark base
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:03:39 +02:00
Frederic Cabassut 4d77f7f819 feat(theme): give the dark colour picker vivid identifiable chips 2026-07-31 12:03:39 +02:00
Frederic Cabassut a2463ac316 test(theme): widen distinguishability threshold headroom
The distinguishability threshold (10) sat only 2 units below the palette's
measured closest pair (coral/peach at 12.0), making it brittle to a minor
future nudge. Lower to 8: still fails the old palette's closest pair
(fog/storm at 7.55) while giving the current palette 4 units of headroom.
Also corrects the spec's lightness-change figure from -0.1 (computed from
rounded HSL) to the precise -0.18.
2026-07-31 12:03:39 +02:00
Frederic Cabassut ffc49f2676 test(theme): replace lightness-band test with pairwise distinguishability
The lightness-band guard was looser than the palette it was meant to catch
(old band 0.0175 vs new 0.0246) and the muted-AA test never failed against
the old palette (5.08:1), so both docs' stated expectations and one spec
claim ("chroma roughly doubles") were measured falsehoods. Replace the band
test with an RGB-distance pairwise check, which does discriminate (old
closest pair fog/storm = 7.5, new closest pair coral/peach = 12), fix the
misleading test comment, and correct the spec/plan docs to match reality.
2026-07-31 12:03:39 +02:00
Frederic Cabassut 9600c62288 feat(theme): rebuild dark note colours as a consistent family 2026-07-31 12:03:39 +02:00
Claude 12f45267f7 feat(editor): close the note editor with browser back navigation
The mobile editor is fullscreen with no backdrop to tap, so the Android
back button / iOS back swipe now dismisses it instead of leaving the
page. The editor pushes a shallow-routing history entry on mount and
closes when it is popped; UI closes pop the entry themselves so history
stays clean. NotesView's hash rewriting now goes through SvelteKit's
replaceState so the router keeps handling popstate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1N5Vi12zTez7LNqRvYjUD
2026-07-15 10:25:54 +02:00
Frederic CabassutandClaude Opus 4.8 d96b20f244 fix(sync): prevent silent content loss in concurrent-edit merges
Address review feedback on the concurrent-edit merge change:

- When no base snapshot is available (e.g. a metadata-only change bumped
  the version without creating a snapshot, or the base was pruned),
  mergeContentUpdate now keeps the incoming edit instead of discarding it
  in favour of the server copy, and logs a warning. Dropping it silently
  lost the user's offline edits.
- Unify getBaseContent into one shared helper in versions-service and drop
  sync's "fall back to the latest snapshot" branch: a base newer than what
  the client actually saw made the 3-way merge treat other users' lines as
  deletions and silently remove them.
- Remove the now-dead mergeContentTwoWay helper.
- Add coverage: null-base keeps incoming (unit), a metadata-bump concurrent
  edit is preserved (integration), and getBaseContent returns null rather
  than a newer snapshot. Verified the null-base cases fail against the old
  drop-incoming behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:11:43 +02:00
Romain LE DISEZandCodex bba0ddd311 fix(sync): merge concurrent note content edits
Create version snapshots before mutating note content so later concurrent
saves can find a base version. Reuse the content merge helper for direct
updates and sync pushes, with tests covering fallback and queued-edit
merge behavior.

Co-authored-by: Codex <codex@openai.com>
2026-07-15 10:11:43 +02:00
Frederic CabassutandClaude Opus 4.8 460f39298b fix(search): preserve unsynced hits and avoid empty-state flash
Address review feedback on the search-results preservation change:

- reconcileSearchResults now keeps a result absent from the canonical
  notes store (a server hit not yet loaded locally, e.g. from another
  device) instead of dropping it, so valid matches no longer vanish
  mid-session. Locally deleted/left notes are still pruned at their call
  sites; trashed notes are still dropped.
- SearchBar commits searchQuery only once its results arrive, so the view
  no longer flashes an empty state over the current notes while a request
  is in flight, and searchQuery stays consistent with searchResults on a
  failed request.
- Expand reconcile unit tests (keep-absent, trashed-drop, mixed order,
  empty) and add an e2e test asserting notes stay visible during an
  in-flight search.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:57:00 +02:00
Romain LE DISEZandGoogle Gemini b802f17504 fix: preserve search results when background sync runs
Keep canonical notes separate from active search results so periodic
reloads no longer replace the filtered view.

Cancel superseded requests and ignore stale responses to prevent older
queries from overwriting newer results. Reconcile results with canonical
notes so updates propagate and removed or trashed notes disappear.

Add unit and end-to-end coverage for reconciliation, background sync,
and out-of-order search responses.

Co-authored-by: Google Gemini <gemini@google.com>
2026-07-15 09:57:00 +02:00
Romain LE DISEZandCodex 59b7a77f3a fix(editor): avoid mobile focus on task checkboxes
Intercept touch checkbox changes before TipTap's focusing handler and
update the task-item transaction directly. This prevents the mobile
keyboard from starting to open while preserving checkbox state changes.

Strengthen the mobile regression test to assert that checking and
unchecking emit no editor focus events.

Co-authored-by: Codex <codex@openai.com>
2026-07-15 09:25:34 +02:00
Frederic CabassutandClaude Fable 5 4b7e346206 fix(editor): keep toolbar and dropdowns above the Android keyboard
Add interactive-widget=resizes-content to the viewport meta so the
Android keyboard shrinks the layout viewport through the native inset
path. Chromium's visualViewport.height can under-report the visible
height by the system nav-bar height, which left the editor toolbar
floating above the keyboard; the native resize path is accurate. iOS
ignores the attribute and keeps using the visualViewport tracking in
NoteEditor.

Position toolbar dropdown portals with window.innerHeight instead of
visualViewport.height: CSS bottom on position:fixed and
getBoundingClientRect() both use layout-viewport coordinates, so
mixing in the visual-viewport height pushed dropdowns behind the
keyboard while it was open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:19:19 +02:00
Romain LE DISEZ 5fb0337d03 fix(editor): address PR 67 review feedback
Restore portal dropdown test IDs so the formatting e2e suite can
find heading and link controls again.

Let the mobile formatting toolbar pan horizontally, keep dropdown
portals from drifting by closing them on scroll or resize, and use
visualViewport height for keyboard-aware positioning.

Remove the app-wide maximum-scale lock, keep editor focus targets at
16px on mobile to avoid input auto-zoom, and move toolbar scrollbar
styling to a top-level scoped style block.

Align the remove-link action with the destructive design token, use
the standard 150ms editor slide-up animation, and replace
save/restore body overflow locking with a resilient body class.
2026-07-10 16:19:19 +02:00
Romain LE DISEZandGoogle Gemini ab8b273400 feat(ui): improve mobile editor layout
Make editor full-screen natively on mobile with back button. Add
visualViewport tracking to keep bottom toolbars above the software keyboard.

disable pull-to-refresh for native PWA feel
prevent auto-zoom on input focus in mobile Safari
prevent background scrolling when editor is open
prevent touch scroll on editor toolbars to stop background scrolling
hide visual gap at bottom of editor when iOS keyboard opens
move note actions to top-right menu on mobile devices
wrap background colors in mobile menu to reduce width
make formatting toolbar scrollable on mobile and extract dropdowns
remove empty else block causing svelte compilation error
add missing data-dropdown attributes to fix click interception in toolbar portals
keep mobile keyboard open when using toolbar menus and prevent right-edge overflow

Co-authored-by: Google Gemini <gemini@google.com>
2026-07-10 16:19:19 +02:00
Claude 5719fc3459 fix: make admin user deletion atomic and surface errors in UI
deleteUser() ran each child-table cleanup as a separate statement, so a
foreign-key violation on any step (e.g. sync_log rows from other users
referencing the deleted user's notes) would abort mid-way and leave the
database in a partial state. The API swallowed the error and the admin
page silently ignored the non-ok response, so clicking Delete and
confirming appeared to do nothing.

- Wrap the full cascade in a single sqlite transaction so the deletion
  is atomic.
- Also remove rows in sync_log, note_user_state, note_collaborators,
  note_versions, and attachments that belong to other users but
  reference the deleted user's notes, so the final DELETE FROM notes
  cannot trip a foreign-key check.
- Surface delete failures in the users settings UI instead of silently
  swallowing them.
- Add data-testids on the user row and delete button to support tests.
- Add e2e coverage for the admin user deletion flow (happy path,
  cancelled confirmation, and a user with notes / tags / attachments /
  preferences / api keys).

https://claude.ai/code/session_015T2HE6VzB64ZmSSyy1Wz4R
2026-04-22 21:45:48 +02:00
Claude ba1b071395 fix: use currentTarget and contains() in blur handler per review
Use e.currentTarget instead of e.target so the reference is always the
contenteditable div, not a descendant <a> tag. Use el.contains() instead
of strict equality to avoid linkifying while focus is still inside the
editable element.

https://claude.ai/code/session_01VZceWPhHMHCA6SELBdCPfN
2026-04-10 21:53:20 +02:00
Claude 86cfc8c5f6 fix: defer contenteditable blur linkification to unblock mobile tap events
When a contenteditable loses focus, the onblur handler immediately modified
innerHTML to linkify URLs. On mobile, this DOM mutation causes a layout reflow
between touchstart and click, making the browser cancel taps on the adjacent
delete button. Deferring via requestAnimationFrame ensures the click event
fires before the DOM is modified.

https://claude.ai/code/session_01VZceWPhHMHCA6SELBdCPfN
2026-04-10 21:53:20 +02:00
Frederic CabassutandClaude Opus 4.6 0a30145996 fix: decode &nbsp; entities from contenteditable in checklist items
Browsers inject &nbsp; in contenteditable elements, which was not being
decoded by unlinkifyHtml, causing double-escaped "&nbsp;" to appear in
checklist item text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:51:14 +02:00
Frederic Cabassut d3b98fddf2 fix: address PR review feedback
- Add noreferrer to rel attribute to prevent Referer header leaks
- Strip newlines from pasted text to prevent checklist corruption
- Add aria-placeholder for screen reader accessibility
- Dismiss LinkPopover on scroll/resize
- Stop keydown propagation on links in NoteCard preview
2026-04-08 22:36:19 +02:00
Frederic CabassutandClaude Opus 4.6 4c13d345fc fix: use Svelte action for contenteditable to prevent cursor destruction
The reactive {#html} binding inside contenteditable replaced innerHTML on
every keystroke, destroying the cursor and causing text duplication.

Instead, use a use:editableContent action that sets innerHTML on mount
and only syncs when the element is NOT focused. Linkification happens
on blur, matching Google Keep's behavior. Update e2e tests to blur
before asserting link visibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 22:36:19 +02:00
Frederic Cabassut 04fdffb111 feat: linkify URLs in NoteCard checklist preview 2026-04-08 22:36:19 +02:00
Frederic Cabassut 5eb7a84a88 feat: linkify URLs in checklist done section 2026-04-08 22:36:19 +02:00
Frederic CabassutandClaude Opus 4.6 beb4209baf fix: decode HTML entities in unlinkifyHtml and fix indentation
- Add entity decoding (&amp; &lt; &gt; &quot;) to unlinkifyHtml to
  prevent double-encoding on contenteditable edit cycles
- Fix indentation of LinkPopover import and state declaration
- Move LinkPopover inside root div
- Add unit test for entity decoding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 22:36:19 +02:00
Frederic Cabassut 377ce4b72c feat: replace checklist text inputs with contenteditable for inline link rendering 2026-04-08 22:36:19 +02:00
Frederic Cabassut 740a89df63 feat: add LinkPopover component for checklist hyperlinks 2026-04-08 22:36:19 +02:00
Frederic Cabassut c753c4b5ce feat: add linkifyText and unlinkifyHtml utilities for checklist hyperlinks 2026-04-08 22:36:19 +02:00
Frederic CabassutandClaude Opus 4.6 b6dbdbe9a5 fix: prevent click-through to NoteCards when closing editor overlay
On installed PWAs, closing the editor by tapping the overlay would cause
the browser's delayed synthetic click to land on the NoteCard underneath,
immediately reopening a different note. Fixed by keeping a transparent
shield in the DOM for 400ms after close to absorb the ghost click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:15:58 +01:00
Frederic CabassutandClaude Opus 4.6 be53b4b26e fix: sync attachment changes to store and skip spurious auto-save
Two issues fixed:

1. Attachment operations (upload, feature, remove) only updated the local
   editor state, not the notes store. Before auto-save, this was masked
   because saveAndClose always called updateNote, whose PATCH response
   refreshed the store. With auto-save skipping redundant saves, the store
   was never updated with attachment changes. Now attachment handlers
   update the store directly.

2. TiptapEditor fires onUpdate on init with round-tripped markdown that
   can differ from the original. Added an autoSaveReady guard to absorb
   this initial normalization as the baseline, preventing spurious saves
   when reopening existing notes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:49:08 +01:00
Frederic CabassutandClaude Opus 4.6 9d1c898b84 fix: address review feedback on auto-save
- Snapshot fields at start of performSave to avoid race with user edits
  during async save (fields could change between await and lastSaved update)
- Only update lastSaved tracking on successful save, so failed saves
  are retried on the next debounce cycle
- Stop restoring title after Untitled fallback in autoSave(), which was
  re-triggering the auto-save effect
- Capture note count before opening editor in empty-note e2e test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:49:08 +01:00
Frederic CabassutandClaude Opus 4.6 07b27c01e7 feat: auto-save notes with debounced persistence
Notes are now automatically saved 2 seconds after the user stops editing,
preventing data loss when the browser tab is closed without closing the
editor first. A beforeunload handler provides an additional safety net.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:49:08 +01:00
Frederic CabassutandClaude Opus 4.6 4f4cc69b9f fix: replace hardcoded test passwords with runtime-generated UUIDs
Generate random passwords in global-setup and share via temp file so
all Playwright workers use the same credentials. Screenshot scripts
use module-level crypto.randomUUID() since they run single-process.
Unit test auth passwords also use randomUUID().

Eliminates security scanner alerts for passwords committed to git.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 10:29:45 +01:00
Frederic CabassutandClaude Opus 4.6 172ab88b33 feat: custom tooltip action replacing native title attributes
Add a reusable `use:tooltip` Svelte action as a drop-in replacement for
native title attributes. Styled to match the retro parchment theme with
JetBrains Mono font, dark background, and 400ms hover delay.

Replace all title attributes in NoteEditor, NoteCard, and NotesView
with the custom tooltip. The action supports top/bottom positioning
and auto-clamps to viewport edges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:09:32 +01:00
Frederic CabassutandClaude Opus 4.6 c82f720b02 fix: add cursor pointer to create crumb buttons
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:04:23 +01:00