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>
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>
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>
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>
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>
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.
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.
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.
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>
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>
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>
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>
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>
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>
Browsers inject in contenteditable elements, which was not being
decoded by unlinkifyHtml, causing double-escaped " " to appear in
checklist item text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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
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>
- Add entity decoding (& < > ") 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
- "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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
- Strip server paths from public attachment responses (new PublicAttachment type)
- Use $page.url.origin instead of window.location.origin (SSR-safe)
- Fix toggle switch left-4.5 → left-[1.125rem] (valid Tailwind)
- Add aria-label to SharingIndicator button for screen readers
- Add opportunistic pruning to IP rate limiter to prevent unbounded growth
- Reduce shared attachment cache to 1 hour (respect share revocation)
- Update OpenAPI schema with PublicAttachment type
- Add unit test asserting server paths are not leaked
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Link "POWERED BY CRUMBS" to crumbs.bretzel.app
- Add dedicated error page for (share) group without "Back to notes"
- Update landing page Note Sharing card to mention public links
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>