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>
Write a markdown coverage table to $GITHUB_STEP_SUMMARY for both unit
and E2E test jobs. Uses a small bash script that parses json-summary
output from the coverage reporters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add V8 coverage for unit tests via @vitest/coverage-v8 and Istanbul
instrumentation for E2E tests via vite-plugin-istanbul. Coverage data
is collected from browser pages after each Playwright test and merged
with nyc. CI uploads both coverage reports as GitHub Actions artifacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pre-build before Playwright instead of building inside webServer
- Reduce retries from 2 to 1 (fix flaky tests instead of masking them)
- Set CI workers to 4 for more parallelism
- Reduce artifact retention from 30 to 7 days
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bearer token authentication now works on all /api/* routes, not just
/api/mcp. If a Bearer token is present it's validated first; otherwise
session cookie auth is used as fallback. Updates settings UI to reflect
the broader scope (tab renamed to "API", REST API curl example added)
and updates the landing page feature card.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Register TaskList and TaskItem TipTap extensions, add "Task list" option
to the List dropdown in the formatting toolbar, and consolidate task list
CSS between the editor and markdown-it preview into shared styles in
app.css.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All isActive() and getAttributes() calls in the template were not
reading the tick prop, so Svelte 5 never re-evaluated them when the
editor state changed. Extracted isActive() and getAttrs() helpers
that consume tick to establish the reactive dependency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use flex-wrap instead of overflow-x-auto for the formatting toolbar —
scrolling clips absolutely-positioned dropdown popovers (CSS forces
both axes to auto). All buttons are now always visible, wrapping to a
second line on narrow screens. Remove the More dropdown entirely since
all items are top-level. Add overflow-hidden to the editor card to
prevent horizontal overflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move undo/redo to the start of the toolbar, promote inline code,
blockquote, code block, and divider to top-level desktop buttons.
The More dropdown is now mobile-only since all items are directly
accessible on desktop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Widen editor modal on desktop (max-w-xl → md:max-w-2xl)
- Extract text alignment from More into its own dropdown with contextual
icon reflecting the active alignment
- Promote undo/redo to top-level buttons on desktop (hidden on mobile,
still available in More dropdown)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace single "Insert table" button with a Table dropdown that shows
contextual row/column operations when the cursor is inside a table:
add row, add column, delete row, delete column, delete table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Install @tiptap/extension-table packages, register Table/TableRow/
TableHeader/TableCell extensions, add Table button to the More dropdown
(inserts a 3×3 grid), and style tables to match the parchment theme.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The popover is inside a top-right positioned indicator, so anchoring
to right-0 keeps it on screen on mobile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents default newline and focuses the content area (textarea or
TipTap editor) when pressing Enter in the title input.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tags are still visible as hashtags in the note content and usable
via the tag filter bar. Reduces card clutter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The default 80ms delay was too short, causing drag to activate
during normal scrolling on mobile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove tests for sort selector UI (removed). Replace with a single
test verifying most recently updated note appears first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add outline-none to both the NoteCard article and the NoteGrid
wrapper div to prevent browser focus outlines when dragging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace drag handle zones with dndzone for whole-card dragging via
long-press on both desktop and mobile. Removes GripHorizontal icon
and dragHandle imports entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the sort mode selector UI. Notes now sort by custom order
first (drag to reorder), falling back to most-recently-updated for
ties. Drag handles are always available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tags get full width for horizontal scrolling, sort buttons sit
below aligned to the right. Side by side on desktop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use scrollbar-width: none and ::-webkit-scrollbar to visually hide
the scrollbar while keeping swipe-to-scroll functional.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add whitespace-nowrap to TagChip so long tags stay on a single line
within the horizontally scrollable filter bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Call loadNotes(filter) when the filter prop changes so the store
gets populated with the correct note subset for each view.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>