Commit Graph
100 Commits
Author SHA1 Message Date
Frederic CabassutandClaude Opus 5 a6f86ec1f0 fix(wu-002): refuse password resets that could never work
Second slice of issue #77. PATCH /api/admin/users/[id] accepted
{ newPassword } for any account and returned 200, but resetPassword()
writes only passwordHash while verifyPassword() matches only rows whose
authProvider is 'password'. So for an OAuth-only account (the
"OAuth-only" checkbox stores 'none') or anyone who has signed in via
OAuth (authProvider becomes the provider name), the endpoint wrote a
hash that could never be used to sign in, returned success, and fired a
"your password was reset" email. It lied.

The endpoint already guards both other self-affecting operations
server-side — "Cannot change your own role" and "Cannot delete your own
account" — but a newPassword targeting the requesting admin succeeded.

Two early returns in the newPassword branch, ordered length -> self ->
eligibility so the existing length 400 keeps precedence. Both are placed
before the resetPassword() call and before the email block: that
ordering is what guarantees no unusable hash is written and no
misleading email is sent, and it is a property of control flow rather
than of an assertion, because neither is observable from a test.
passwordHash never leaves the server (toUser omits it) and
isEmailConfigured() is false under e2e, so an assertion either way would
pass for a broken implementation too.

Eligibility comes from canResetPassword() rather than an inline
comparison, so the endpoint and the settings UI cannot drift; the 400
message comes from passwordResetBlockedReason() so the API reason and
the UI explanation are the same string.

DoD items verified:
- [x] 1. eligible user other than self still 200s, new password works
- [x] 2. authProvider 'none' returns 400 naming the reason
- [x] 3. positive control: an eligible user CAN log in after a reset
- [x] 4. targeting own id returns 400 naming the reason
- [x] 5. sub-8-char password still 400s for length, beating eligibility
- [x] 6. both guards precede the write and the email (diff-verified)
- [x] 7. role, revokeSessions and DELETE byte-unchanged (15+/0-)
- [x] 8. eligibility imported from wu-001, not re-implemented
- [x] 9. declarative Given/When/Then, distinct victim emails throughout

Verified independently by the orchestrator: pnpm check 0 errors /
14 pre-existing warnings; pnpm test:unit 306 passed / 26 files;
pnpm build && pnpm test:e2e admin-users.spec.ts 14 passed (7 auth-setup
+ 3 pre-existing + 4 new). The reviewer additionally reverted the
endpoint to its pre-change form and confirmed both guard scenarios fail
with "Expected: 400, Received: 200", so they are genuine regression
tests rather than assertions of behaviour that already held.

Reviewed-by: adversarial-review (PASS)

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 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 1ced23033e fix(e2e): match note-card locator on exact title, not substring content
noteCard() filtered on hasText, matching against a card's *entire*
rendered text — with note-linking, a card's content preview can now
render a linked note's title as plain text (a stripped note-link), so
a substring match against the whole card ambiguously resolves to more
than one card whenever one note's title happens to appear inside
another's preview. Scoped the match to the card's <h3> title element
with an anchored exact-match regex instead.

This was failing CI on two tests: the note-linking suite's own
backlink scenario, and — since test note titles aren't otherwise
isolated across files in the same run — an unrelated notes-crud test
whose note title happened to collide with a leftover "Linker Note"
card's rendered preview.

Verified: full e2e suite (114/114) passes locally against this fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:51:08 +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
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 CabassutandClaude Sonnet 5 dc20ac0f60 fix(ci): pin e2e Docker image to playwright v1.62.1 to match npm package
The @playwright/test npm package was bumped to ^1.62.1 during an earlier
dependency-vulnerability pass, but the CI container image stayed pinned
to v1.58.0-noble — Playwright requires the container's bundled browser
binaries to exactly match the npm package's version, so e2e jobs have
been failing with "Executable doesn't exist" ever since that bump landed
on main, unrelated to any feature branch's own changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:30:01 +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 35fceac5a1 docs: add note-linking implementation plan
11 TDD tasks covering schema, extraction/sync/backlink service functions,
the NoteLink tiptap node (parse/serialize + NodeView + title index),
toolbar note-search UI, click-to-navigate wiring, the backlinks UI
section, preview/public rendering, and e2e coverage.

Self-review caught and fixed three real bugs before this was usable:
a test referencing a USER_ID constant that doesn't exist in the actual
notes-service.test.ts (it's OWNER_ID), a posAtDOM-based position lookup
in the NodeView's remove button where tiptap's own getPos() is the
correct API, and a title-index gap where trashed notes remain in a
note's own IndexedDB cache (only collaborators have them filtered out
at sync time) — so the index needed its own explicit trashed filter or
a trashed note's links would never grey out for its owner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 15:25:55 +02:00
Frederic CabassutandClaude Sonnet 5 895edc05a3 docs: address review gap in note-linking spec — link removal affordance
Re-reviewed the committed spec and found instant-navigate-on-click left
no way to ever remove or repoint an existing note-link, since external
links solve this via a click-triggered popover that navigation now
replaces. Resolved with a hover-revealed x on the chip (touch-visible
by default), keeping the chosen click-to-navigate behavior intact.

Also verified three feasibility risks while reviewing: tiptap-markdown's
custom node serialization API is real and documented; version restore
already goes through updateNote() so link-sync comes for free; and the
line-based content-merge algorithm is markdown-agnostic, so concurrent
edits can't corrupt a note-link reference any more than any other text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 14:59:03 +02:00
Frederic CabassutandClaude Sonnet 5 50041d2298 docs: add note-linking design spec
Brainstormed with the user for issue #74. Notes can reference each
other via the existing link toolbar, rendered as a live tiptap node
(not a mark) so the label always tracks the target's current title,
plus a backlinks section on the referenced note.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 14:45:06 +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 Cabassut 2095c32347 docs: document the dark palette and the card-vs-chip distinction 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 3041de4b2f feat(theme): replace dark surfaces, borders and accents with the warm ink palette 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
Frederic CabassutandClaude Opus 5 365f941240 docs: add dark palette rework implementation plan
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:03:39 +02:00
Frederic CabassutandClaude Opus 5 edee5f7cd4 docs: add dark theme palette rework design spec
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:03:39 +02:00
Frederic CabassutandClaude Opus 5 b5109e7522 fix(sharing): make public note colors follow the visitor's theme
The public share page picked the note background from the light palette
unconditionally, while app.html applies data-theme="dark" on every page.
A visitor in dark mode therefore got a light pastel card rendered with the
dark theme's light text colour — washed out and barely readable.

Emit both palettes as custom properties and let [data-theme] select one, so
the correct colour lands on the first paint of this SSR page instead of
flashing after hydration.

Reported by a user over email.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:03:39 +02:00
Frederic CabassutandClaude Opus 4.8 fc9ab5b237 test(e2e): deflake dark-mode by awaiting full editor close in createNote
createNote returned as soon as the note card appeared (editor merely
"closing"), but the editor now dismisses via an async history.back() plus a
400ms onClose (added with close-on-back navigation, #71). A following
page.goto could race that in-flight navigation and abort it
(net::ERR_ABORTED) — surfacing only under the slower coverage-instrumented
CI build. Wait for the editor overlay to fully unmount so the close
navigation settles before createNote returns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:59:01 +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
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
Frederic CabassutandClaude Opus 4.8 32c39b0000 test(editor): assert checkbox toggle updates the editor document
Read the ProseMirror document's taskItem `checked` attribute after
tapping the mobile task checkbox, not just the native checkbox widget.
The widget toggles regardless of whether the fix's setNodeMarkup
transaction runs, so the previous assertions could not detect a
widget/document desync. Verified this fails against the pre-fix code
and passes with the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.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
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 Cabassut 70c258f3b8 chore: remove design spec and plan docs from feature branch 2026-04-08 22:36:19 +02:00
Frederic Cabassut 4c1083e14d chore: remove playwright-mcp logs 2026-04-08 22:36:19 +02:00
Frederic CabassutandClaude Opus 4.6 49c28a33ff chore: remove accidental files from feature branch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 0ece0b7b7f test: add e2e tests for checklist hyperlink support 2026-04-08 22:36:19 +02:00
Frederic Cabassut f6642ac7b3 test: update e2e tests for contenteditable checklist inputs 2026-04-08 22:36:19 +02:00
Frederic Cabassut 653e990071 feat: linkify URLs in shared note checklist view 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 17216bd17d style: add checklist-link CSS and contenteditable placeholder styles 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 042c4c06e5 fix: rename test note to avoid collision with offline test
The auto-save test used "Survive Reload" which collides with the same
title in offline.spec.ts when tests run in parallel with a shared DB.

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 3f81f22026 fix: align checkboxes in richtext task list previews
Add missing nested task list rule for .prose context (matching TipTap's
existing rule) and adjust checkbox vertical alignment with margin-top.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:46:13 +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
Frederic CabassutandClaude Opus 4.6 548aa14d63 fix: add cursor pointer to create crumb button
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:03:26 +01:00
Frederic CabassutandClaude Opus 4.6 c66217e6f1 feat: add checklist shortcut to create button
Add a checkbox icon next to the "Add a crumb..." button that opens a new
note directly in checklist mode, similar to Google Keep's quick-create
bar. The NoteEditor accepts a new initialChecklistMode prop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:53:01 +01:00
Frederic CabassutandClaude Opus 4.6 6c9781fb24 fix: sharing indicator popover opens downward to avoid overflow clipping
The popover was opening upward (bottom-full) which got clipped by the
NoteCard's overflow-hidden. Changed to open downward (top-full) so it
stays within the card's visual area.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:50:48 +01:00
Frederic CabassutandClaude Opus 4.6 b44cd40e36 fix: align nested checkboxes in rich text task lists
Nested TipTap task lists (indented children) broke the flex layout
because the nested ul was treated as an inline flex child. Now the
li uses flex-wrap with align-items: flex-start, and nested task lists
take full width with left padding for indentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:49:10 +01:00
Frederic CabassutandClaude Opus 4.6 8b9f9dfee8 feat: add overflow menu with checklist actions and action bar cleanup
- Add vertical dots (⋮) overflow menu to NoteEditor bottom action bar
- New checklist actions: "Delete checked" and "Uncheck all"
- Move secondary actions to menu: checklist toggle, markdown toggle,
  version history, move to trash
- Promote archive to the action bar for quick access
- Keep in bar: color picker, image upload, share, archive, ⋮, close
- Menu renders as fixed-position portal to escape card overflow-hidden
- Click-outside handler closes the menu
- Update e2e tests to use overflow menu for checklist/markdown toggles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:47:02 +01:00
Frederic CabassutandClaude Opus 4.6 5a2a63e2bb fix: prevent 3-way merge from reverting sequential single-user edits
The server's 3-way content merge was incorrectly triggering on sequential
edits by the same user. When a user checked then unchecked a checklist
item, the merge saw local === base (both unchecked) and kept the server's
checked state, silently discarding the uncheck.

Now the merge only runs when there's an actual concurrent edit
(baseVersion < existing.version), meaning another user saved between the
client's load and this save. For single-user sequential edits, the
client's content is used directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 18:51:14 +01:00
Frederic CabassutandClaude Opus 4.6 6eb35e9df2 perf: add BuildX + GHA caching to CI docker validation build
CI only needs to verify the Dockerfile builds on amd64 — multi-arch
is handled by the release workflow with native ARM runners. Switching
from bare `docker build` to BuildX with GHA layer caching speeds up
subsequent builds significantly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:28:48 +01:00
Frederic CabassutandClaude Opus 4.6 f1b42f115a perf: parallelize multi-arch Docker builds with native ARM runners
Replace sequential QEMU-emulated builds with parallel native builds
using a matrix strategy (ubuntu-latest for amd64, ubuntu-24.04-arm
for arm64) and manifest merging. This eliminates the ~5-10x QEMU
overhead and should cut Docker build time roughly in half.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:06:37 +01:00
Frederic CabassutandClaude Opus 4.6 2267474cd5 fix: address PR review — a11y, checked detection, test fixes
- Remove unused flushSync import
- Fix removeItem single-item branch: emit change and trigger reactivity
- Remove misleading role="button" + tabindex from drag handle
- Use anchored regex for checked detection instead of line.includes('[x]')
  in NoteCard and public share page (prevents false positive on item text)
- Fix NoteCard preview E2E test: scope assertion to specific card, use
  unique title to avoid collision
- Restore arrow key navigation E2E test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:51:46 +01:00
Frederic CabassutandClaude Opus 4.6 0124c5e7f6 fix: unchecked child repositions after its parent and siblings
When unchecking a child from the done section, it stayed at the end of
the items array (where done items live), appearing as the last active
item instead of under its parent. Now repositions after the last sibling
or parent in the array.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:51:46 +01:00
Frederic CabassutandClaude Opus 4.6 ffcabad30e docs: add nested checklists to features and architecture docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:51:46 +01:00
Frederic CabassutandClaude Opus 4.6 dee5129d59 test: add E2E tests for nested checklist interactions
7 new scenarios: Tab/Shift+Tab, parent cascade check, child done with
parent label, uncheck parent restores group, nesting persists on reopen,
Enter on child creates sibling, NoteCard preview indentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:51:46 +01:00
Frederic CabassutandClaude Opus 4.6 087fe8c713 feat: NoteCard and public share page support nested checklist items
Update checklist preview parsers to detect 2-space child prefix and
render indented items with extra left padding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:51:46 +01:00
Frederic CabassutandClaude Opus 4.6 2306883503 feat: nested checklist UI — indentation, parent labels, drag-to-indent
Checklist component changes:
- Import logic from checklist utility module
- Tab/Shift+Tab indent/outdent with sibling adoption
- Indented rendering (pl-8) for child items in active section
- Done section groups checked children under read-only parent labels
- Enter on child creates sibling at same indent level
- Backspace on parent orphans children to top-level
- Directional lock drag: horizontal = indent/outdent, vertical = reorder
  (uses window capture-phase listeners + pending indent applied after
  DnD finalize to avoid corrupting svelte-dnd-action internal state)
- flushSync for state updates from window event listeners

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:51:46 +01:00
Frederic CabassutandClaude Opus 4.6 9fb8acc719 feat: checklist utility module with parentId and nesting logic
Extract checklist logic into testable utility module:
- ChecklistItem type with parentId field for single-level nesting
- parseChecklist/serializeChecklist with 2-space prefix for children
- toggleItemWithCascade: parent check/uncheck cascades to children
- indentItem/outdentItem with sibling adoption rules
- 21 unit tests covering parse, serialize, toggle, indent, outdent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:51:46 +01:00
Frederic CabassutandClaude Opus 4.6 8155bd4ff5 fix: address PR review — unique test titles and assertion between clicks
- Rename "Groceries" to "Focus Test List" to avoid title collision
- Add assertion (1 done) between consecutive checkbox clicks to prevent
  race condition where second click toggles the same item back

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 20:27:38 +01:00
Frederic CabassutandClaude Opus 4.6 f9d29a192a feat: arrow key navigation between checklist items
ArrowUp/ArrowDown move focus between active checklist inputs.
At list boundaries, focus stays on the current item (no wrap).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:17:58 +01:00
Frederic CabassutandClaude Opus 4.6 dd100683c9 fix: rename test note to avoid name collision in CI
- "Shopping" matched existing "Shopping List" note from another test
- Renamed to "Groceries" to avoid strict mode violation
- Reworded comments: "interleaved" → "checked items before active ones"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:11:00 +01:00
Frederic CabassutandClaude Opus 4.6 e0eca4228b fix: checklist Enter key focuses new item on reopened notes
The addItem() focus logic used positional indexing (inputs[afterIndex+1])
but afterIndex came from the full items[] array while DOM inputs only
render active (unchecked) items. On reopened notes, checked items are
interleaved in the parsed content, causing the index offset to be wrong.

Fix: use data-item-id attribute for targeted querySelector lookup.
Test: reopened note with interleaved checked/unchecked items.

Introduced in e5688c9 (active/done split).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:11:00 +01:00
Frederic CabassutandClaude Opus 4.6 4eb97b86f9 fix: address PR review comments
- Wrap readFile in try/catch, return 404 for missing attachment files
- Remove unused CollaboratorPopover import from NoteCard
- Make darkMode a proper $state rune for reactivity (add svelte-runes
  polyfill for Vitest so $state works in unit tests)
- Add error toasts for failed share link toggle (non-2xx responses)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Opus 4.6 552c2b6265 fix: ensure dark mode E2E test creates a note before asserting colors
The "note cards use dark colors" test was conditional on notes existing,
making the assertion potentially vacuous. Now creates a note first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic Cabassut 09c8e290a9 docs: add dark mode to features and architecture docs 2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Opus 4.6 7a27952713 fix: eliminate E2E dark mode test flakiness
- Use test.describe.serial to prevent parallel preference conflicts
- Set up waitForResponse listener BEFORE click (response may arrive
  before listener is registered)
- Add waitForLoadState('networkidle') after navigations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Opus 4.6 7c19f4a30a chore: add .superpowers/ to gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic Cabassut e0aea4bc2b test: add E2E tests for dark mode toggle and persistence 2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Sonnet 4.6 5dfde02749 feat: use dark note card colors in all components
Switch NoteCard, NoteEditor, and ColorPicker from static NOTE_COLORS
lookups to getNoteColor(color, getIsDarkMode()), so inline background
styles react to the active theme. Also fix the theme preference button
type cast in settings/preferences to resolve the svelte-check error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic Cabassut fa29321a62 feat: add theme toggle to preferences settings 2026-03-21 08:47:20 +01:00
Frederic Cabassut cdf242a692 feat: add reactive theme watcher in root layout 2026-03-21 08:47:20 +01:00
Frederic Cabassut d6638260a3 feat: add FOUC-prevention script for dark mode 2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Sonnet 4.6 b820ee3368 feat: add dark note card colors and getNoteColor helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Sonnet 4.6 80d18478da feat: add theme utility with applyTheme and isDarkMode
Adds src/lib/utils/theme.svelte.ts with applyTheme() (sets data-theme
attribute and meta theme-color) and getIsDarkMode(). Uses a plain
variable instead of $state since Vitest's node environment does not
compile Svelte 5 runes in .svelte.ts files. Adds jsdom dev dependency
and $app/environment alias in vitest config to support DOM-level unit
tests; fixes preferences.test.ts to include the theme default added
in Task 2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic Cabassut 35cd888547 feat: add theme preference type (system/light/dark) 2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Sonnet 4.6 73b07a3be6 feat: add dark theme CSS variables and grid-dot extraction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Opus 4.6 57b9d89e2e docs: add dark mode implementation plan
10-task plan covering CSS variables, preferences, FOUC prevention,
reactive theme application, component updates, and testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Opus 4.6 e9aafd4f92 docs: revise dark mode spec after review
Address reviewer feedback: null-check meta element, use var(--border-subtle)
for card-shadow, specify .svelte.ts for reactivity, add data-testid attrs,
clarify SSR guards, resolve (auth) layout via root layout, defer landing page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Opus 4.6 9ea45b538c docs: add dark mode design spec
Design spec for issue #26 — dark parchment theme with system/light/dark
toggle in Settings > Preferences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:47:20 +01:00
Frederic CabassutandClaude Opus 4.6 9f78f130d3 docs: add grid screenshot to README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:58:39 +01:00
Frederic CabassutandClaude Opus 4.6 dfe791ea44 fix(screenshots): force exit after capture to kill orphaned server
pnpm spawns vite as a grandchild process that SIGTERM doesn't reach,
leaving the CI job hanging after screenshots are captured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:55:32 +01:00
Frederic CabassutandClaude Opus 4.6 54f0bb847c fix(screenshots): use HTTP polling for server readiness in CI
Replace stdout-based detection with HTTP polling (fetch + retry).
Stdout detection is unreliable in CI where pnpm buffers output.
Increased timeout to 60s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:49:11 +01:00
Frederic CabassutandClaude Opus 4.6 2d15e31bc4 chore: fix stale comment about pinned note
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00
Frederic CabassutandClaude Opus 4.6 9c1d533dad chore(screenshots): remove unused pixel-art generator
No longer needed after removing image attachments from seed data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00
Frederic CabassutandClaude Opus 4.6 79c6eeb4dd fix(screenshots): final content tweaks and regenerated screenshots
- Remove title from self-host manifesto note (content-only card)
- Regenerated all 9 screenshots on rebased main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00
Frederic CabassutandClaude Opus 4.6 b50ee51f82 fix(screenshots): improve seed data content and remove attachments
- Remove image attachments and attachments screenshot
- Remove pinned note for cleaner hero grid
- Enrich note content with markdown (headings, bold, italic, blockquotes, lists)
- Replace "Ancient Flute" with "Coding Playlists" (ties to Navidrome)
- Shorten titles, remove content that repeats title
- Remove hashtags from checklist notes (rendered as items)
- Use bretzel persona "Salzig Bretzel" for collaborator
- Update services: Crumbs, 42, Dokploy, Outline, Navidrome, RetroAssembly, Umami

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00
Frederic CabassutandClaude Opus 4.6 17f066d9b1 chore(screenshots): add generated landing page screenshots
10 screenshots captured via automated pipeline:
- 7 desktop (1280x800): grid, editor, checklist, sharing, history, attachments, API
- 3 mobile (390x844): grid, editor, checklist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00
Frederic CabassutandClaude Opus 4.6 d38cb88624 feat(website): update landing page with new screenshot references
Replace 3 desktop + 3 mobile screenshots with 7 desktop + 3 mobile.
New screenshots: checklist, sharing, history, attachments, API/MCP.
Remove obsolete screenshot-tags.png and screenshot-mobile-sidebar.png.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00
Frederic CabassutandClaude Opus 4.6 f28c5671c1 ci: add manual workflow to regenerate landing page screenshots
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00
Frederic CabassutandClaude Opus 4.6 7fdfa305bd feat(screenshots): add main orchestrator and fix capture selectors
- Use @playwright/test instead of playwright (matches devDependencies)
- Use card-specific locators to avoid substring content matches
- Close history panel before editor to avoid overlay blocking
- Add Origin header for multipart upload CSRF compliance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00
Frederic CabassutandClaude Opus 4.6 65eb7936ca feat(screenshots): add desktop and mobile capture logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:32:16 +01:00