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>
- FEATURES.md: add public link sharing feature details
- ARCHITECTURE.md: add shared_notes table to schema section
- openapi.yaml: add share create/revoke, public note, and public
attachment endpoints with SharedNoteData schema
- Regenerate API.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unit tests:
- shares-service: create, idempotent create, revoke, get, batch fetch
- ip-rate-limit: under/over limit, IP independence, window reset
E2E tests:
- Create public share link via UI
- Public page renders without auth
- Revoke share returns 404
- Trashed note share returns 404
- Globe icon on shared card
- Copy link to clipboard
Also fix sharing-indicator test-id in existing sharing.spec.ts
and add shared_notes table to test-helpers and db/index.ts init.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create SharingIndicator component with Globe/Users icon priority
- NoteCard shows Globe icon (gold) for public shares, Users for collabs
- NoteEditor share button reflects public link state
- Share token propagated through store on create/revoke
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add toggle switch, URL display, and copy button for public share
links above the existing collaborators section. Gold border when
active, retro-styled toggle with hard shadows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- (share) layout group with minimal centered layout
- Server-side load function fetches shared note by token
- Read-only page renders markdown, checklists, and images
- Rate limited via IP-based limiter
- Retro parchment styling with "POWERED BY CRUMBS" footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- POST/DELETE /api/notes/[id]/share for creating/revoking public links
- GET /api/shared/[token] for fetching shared note data (no auth)
- GET /api/shared/[token]/attachment/[attachmentId] for public images
- Add /s/ and /api/shared to PUBLIC_PATHS in auth middleware
- Add IP-based rate limiter (60 req/min) for public endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create shares-service.ts with fetchSharesForNotes, createShare,
revokeShare, getShareByNoteId, and getSharedNote. Integrate share
tokens into hydrateNotes pipeline alongside tags, attachments,
and collaborators.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add sharedNotes table with noteId primary key, unique token, and
cascade delete. Add NoteShare and SharedNoteData interfaces.
Extend Note type with optional shareToken field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add max-h-[17rem] overflow-hidden on note cards so checklist notes
don't force text notes in the same grid row to stretch with
truncated content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add provenance: false to docker/build-push-action to prevent BuildKit
from generating SLSA attestation entries that appear as unknown/unknown
in the image manifest.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add QEMU emulation and multi-platform build config so ARM64 users
can pull the image without "no matching manifest" errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a script that scans src/routes/api/ for exported HTTP methods and
generates an OpenAPI 3.1 spec at /api/openapi.json. Runs automatically
before each build.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The PWA manifest icons were a generic pretzel emoji on a dark background,
not matching the app's pixel-art pretzel favicon. Regenerated from the
favicon.svg source.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SvelteKit sets Vite's base to './' for client builds, causing the PWA
plugin to generate relative SW paths (./sw.js). On sub-routes like
/settings or /tag/foo, this resolves to /settings/sw.js which returns
HTML, breaking SW registration. Force scope and buildBase to '/'.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The maskable purpose caused Android to crop the already-round icon
into its adaptive shape, resulting in a square-inside-circle appearance.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Active service workers change the fetch lifecycle enough that Playwright's
waitForResponse() misses SW-mediated responses, causing preferences tests
to timeout. Block SW registration during e2e tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Workbox route matching intercepts all HTTP methods, which caused PUT/POST
requests (e.g. preferences updates) to be handled by the SW strategy
handler instead of passing through directly. Restrict API and attachment
cache rules to GET-only using function-based urlPattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vite-pwa/sveltekit does not auto-inject SW registration into SSR HTML.
Add explicit registration via virtual:pwa-register in root layout onMount,
inject webmanifest link via virtual:pwa-info, disable SvelteKit's built-in
SW registration to avoid conflicts, and add required dependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Navigation requests had no caching rule, so refreshing offline showed the
browser's default "you are offline" page. Add NetworkFirst runtime caching
for navigations (3s timeout), cache Google Fonts for offline use, remove
conflicting static webmanifest in favor of the plugin-generated one.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add SMTP configuration example to README install section
- Add note version history, sharing, and email notifications to README feature list
- Expand FEATURES.md with all 9 email notification types and version history section
- Add note sharing, version history, and email alerts feature cards to landing page
- Add all OAuth/SSO env vars to docker-compose.yml with empty defaults
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On mobile, tapping a note card could trigger action buttons in the
editor that rendered at the same coordinates (ghost click). Suppress
pointer events on both toolbars for 150ms after mount — matching the
pop-in animation — so the synthetic click passes harmlessly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Playwright's setOffline(false) doesn't reliably emit the browser's
'online' DOM event in headless/CI environments. Manually dispatch it
as a safety net so the sync client always triggers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Run E2E tests inside mcr.microsoft.com/playwright container which has
Chromium pre-installed, eliminating the ~30s browser download on each run.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three sources of flakiness fixed:
1. **Preferences sync race** — page.goto() triggers a full reload which
re-runs initPreferences() + syncPreferencesFromServer(). On slow CI,
the async server sync can return stale data from parallel test workers
that share the same user's server-side preferences. Fixed with
gotoWithStablePrefs() which blocks the one GET /api/preferences sync
request, so initPreferences() falls back to localStorage (always correct).
2. **localStorage not reset** — resetPreferences() only reset the server.
After a full page reload, loadFromLocalStorage() could return stale
values from previous test runs. Now resets both server and localStorage.
3. **API key name collision** — parallel test runs created keys with the
same name ("Settings Test Key"), causing Playwright strict-mode
violations when multiple elements matched. Fixed with Date.now()
unique suffixes.
Verified: 167/167 settings tests pass (20x repeat), 173/173 full suite (5x).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add docs/TESTING.md with 10 BDD testing best practices
- Add createNote() helper to fixtures to DRY up state setup
- Add createChecklistNote() helper in checklist tests
- Rename scenario titles to describe outcomes, not actions
- Replace imperative Given steps with createNote() helper
- Fix fragile conditional in tag filtering test (silent pass)
- Remove implementation details from Gherkin comments
- Improve Given/When/Then rhythm consistency across all tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each preference test now resets to default preferences via API at the
start, ensuring parallel tests don't pollute each other's state.
Also await PUT response before reload in persist test to prevent
race condition.
Add /settings/about page showing version, project description,
Bretzel ecosystem info, open source details, and tech stack.
Move version display from sidebar to About page and align
Settings nav item styling with other sidebar items.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Settings tests set defaultNoteMode to 'markdown' without resetting,
causing parallel sharing test to timeout when TiptapEditor wasn't
rendered (textarea shown instead). Two fixes:
- Reset preferences in settings tests after changing them
- Create note via API in sharing test (tests sharing, not note creation)
Move Profile page to /settings/profile and add a 302 redirect from
/settings to /settings/preferences so the first settings tab is
Preferences, not Profile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Track mousedown origin so click-outside-to-close only fires when both
mousedown and mouseup occur on the overlay backdrop, not when dragging a
text selection from inside the editor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove duplicate bold formatting test from notes-crud.spec.ts
- Trim formatting.spec.ts from 9 to 5 tests (remove italic, inline code,
toolbar bold, toolbar visibility — same mechanism as bold shortcut)
- Remove hardcoded waitForTimeout(500) from search.spec.ts (Playwright
assertions already retry with built-in timeouts)
- Restructure sharing.spec.ts: 9 serial tests → 5 parallel self-contained
tests using collabPage fixture and API-based setup
- Add collabPage fixture to helpers/fixtures.ts for sharing tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make db a required first parameter on all service functions (notes-service,
api-utils, tags, collaborators, attachments, rate-limit, sync/server).
This enables unit testing with in-memory databases via createTestDb().
Also removes the NODE_ENV=test bypass from rate-limit and adds optional
now parameter for time control in tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>