Commit Graph
640 Commits
Author SHA1 Message Date
SnapOtter f0941e4aec fix: resolve type errors in selection/crop/transform tools 2026-05-06 23:37:31 +08:00
SnapOtter 0f42ceca83 Merge branch 'worktree-agent-a82b6128' into feat/image-editor
# Conflicts:
#	apps/web/package.json
#	apps/web/src/stores/editor-store.ts
#	apps/web/src/types/editor.ts
#	pnpm-lock.yaml
2026-05-06 23:30:42 +08:00
SnapOtter 01dcfa624b feat: add selection, crop, transform tools and supporting infrastructure for image editor
Implements Agent 2 deliverables for the image editor: move tool with Konva
Transformer and smart guide snapping, rectangular/elliptical marquee and
lasso selection with marching ants animation, magic wand flood fill, crop
tool with darkened overlay and rule-of-thirds grid, free transform with
numeric inputs, context menu with z-ordering and clipboard operations,
rulers with drag-to-create guides, smart alignment guides, and canvas/image
resize dialogs. Also creates the shared editor types and Zustand store with
full layer management, z-ordering, clipboard, guide, and document operations.
2026-05-06 23:29:29 +08:00
SnapOtter 1d59997db5 Merge branch 'worktree-agent-a39efeb4' into feat/image-editor
# Conflicts:
#	apps/web/src/components/editor/editor-right-panel.tsx
2026-05-06 23:29:25 +08:00
SnapOtter 2ae97a6c88 feat: add history panel, keyboard shortcuts, export dialog, and navigator minimap
Implement Agent 7 features for the image editor:
- History panel with undo/redo list, action icons, and state jumping
- 51+ keyboard shortcuts via react-hotkeys-hook (tools, modifiers, clipboard)
- Export dialog with PNG/JPEG/WebP format, quality, resize, clipboard copy
- Navigator minimap with viewport rectangle, drag-to-pan, zoom slider
- Autosave/recovery system with localStorage and recovery banner
- Project save/load (.snapotter JSON format)
- Unsaved changes warning via beforeunload
- Paste from system clipboard support
2026-05-06 23:28:29 +08:00
SnapOtter 7c5bd10e80 Merge branch 'worktree-agent-ac1b61d3' into feat/image-editor 2026-05-06 23:24:55 +08:00
SnapOtter 87bbf2f889 feat: add drawing and shape tools for image editor
Implement 8 tool hooks, 7 options bar components, and 1 fill dialog:

Tools:
- Brush: Konva.Line with smooth strokes, hardness via shadowBlur
- Eraser: brush with globalCompositeOperation destination-out
- Shape: Rectangle, Ellipse, Line, Arrow, Polygon, Star with Shift constraint
- Fill (Paint Bucket): scanline flood fill with tolerance and contiguous mode
- Gradient: linear/radial via drag gesture on offscreen canvas
- Clone Stamp: Alt+click source, paint cloned pixels with aligned mode
- Dodge/Burn/Sponge: per-pixel brightness/saturation manipulation by range
- Pixel Brush: blur (box blur), sharpen (unsharp), smudge (directional blend)

Options bars:
- Brush: size/opacity/hardness sliders
- Shape: type selector, fill/stroke color, stroke width, corner radius, sides
- Fill: tolerance slider, contiguous checkbox
- Gradient: linear/radial toggle, opacity, reverse
- Clone Stamp: size/opacity/hardness, aligned toggle
- Dodge/Burn: tool toggle, range dropdown, exposure/flow sliders
- Pixel Brush: tool toggle, size, strength

Common:
- Fill Dialog: Shift+Backspace modal with foreground/background/color/white/black/50% gray
2026-05-06 23:24:34 +08:00
SnapOtter 6238925c38 Merge branch 'worktree-agent-a87ea474' into feat/image-editor 2026-05-06 23:24:09 +08:00
SnapOtter c4bcb12173 feat: add adjustments, filters, levels, curves, histogram, and effects panels for image editor
Implements Features 17, 18, 35, 36, 41, 42, 43 for the editor:
- SliderRow reusable component (label + range + numeric input)
- AdjustmentsPanel with 8 adjustment sliders (brightness, contrast,
  hue, saturation, luminance, exposure, vibrance, warmth)
- Auto adjustments: Auto Tone, Auto Contrast, Auto Color, Auto Enhance
- Levels section with per-channel control, histogram display, black/white
  point, gamma, and output range sliders
- Curves section with 200x200 interactive graph, cubic spline
  interpolation, per-channel support, and 9 presets
- 12 filter controls: toggle (grayscale, sepia, invert, solarize) and
  slider (blur, sharpen, noise, pixelate, emboss, posterize, threshold,
  kaleidoscope)
- Additional blur types: motion blur, radial blur, surface blur
- Vignette with amount/midpoint/roundness/feather sliders
- Grain with amount/size/roughness sliders
- HistogramPanel with RGB channel overlays and stats (mean, stddev, median)
- Reset All and Apply action buttons
- 150ms debounce on adjustment sliders, 300ms on histogram updates
2026-05-06 23:23:45 +08:00
SnapOtter 7e2a7adbd2 Merge branch 'worktree-agent-af61ea3e' into feat/image-editor 2026-05-06 23:23:24 +08:00
SnapOtter a9547f5944 feat: add color system panel, eyedropper tool, and color swatch components
Color panel with foreground/background swatches, react-colorful picker
popover, HEX/RGB/HSL input modes, recent colors row, swap (X) and reset (D).
Eyedropper tool samples composited pixels via stage.toCanvas() with 1x1/3x3/5x5
sample size options, alt+click for background color.
2026-05-06 23:23:05 +08:00
SnapOtter fedb7bc2e3 feat: add layers panel with blend modes, effects, and drag reorder
Implements the layer management panel for the image editor (Features 16, 40):
- Blend mode dropdown (16 modes) and opacity slider for active layer
- Layer list with visibility/lock toggles, thumbnails, and inline rename
- Pointer-based drag reorder (not HTML5 drag API)
- Right-click context menu: Duplicate, Merge Down, Flatten All, Delete
- Layer effects section: Drop Shadow, Inner Shadow, Outer Glow, Stroke
- Each effect with enable/disable toggle and full parameter controls
- Wired into EditorRightPanel as the "Layers" tab content
2026-05-06 23:22:24 +08:00
SnapOtter 9eae8369bb feat: wire editor route, sidebar, mobile nav, and i18n 2026-05-06 23:14:30 +08:00
SnapOtter 08a2119564 feat: add editor layout components, welcome screen, and editor page
Create the four-zone editor layout shell (toolbar, options bar, status
bar, right panel), welcome screen with drag-and-drop and new document
dialog, and the main editor page with clipboard paste and URL loading.
2026-05-06 23:12:59 +08:00
SnapOtter 155783ca86 feat: add canvas system and utility components for image editor
Create canvas zoom hook with smooth Konva.Tween transitions, icon button
component, cursor management with brush overlay, and editor canvas with
checkerboard background, ResizeObserver sizing, and grid overlay.
2026-05-06 23:08:32 +08:00
SnapOtter eb6f3c33ac feat: add image editor type definitions and zustand store
Add complete type system for the editor (ToolType, CanvasObject, EditorLayer,
EditorState, etc.) and the central Zustand store with zundo undo/redo middleware,
50-entry history limit, 500ms throttle, and O(1) _historyVersion equality checks.
2026-05-06 23:05:33 +08:00
SnapOtter 711ddf8deb chore: add konva, react-konva, and editor dependencies 2026-05-06 23:01:47 +08:00
SnapOtter d705c863e3 Merge feat/transparency-fixer: add PNG Transparency Fixer tool
New AI-powered tool that fixes fake transparent PNGs in one click.
Uses BiRefNet HR-matting (2048x2048) with Sharp defringe post-processing.
2026-05-06 21:57:46 +08:00
SnapOtter 8f9ba701be fix: QR code logo causes preview to vanish and become unrecoverable
The QR code generator's logo feature was broken in production (Docker)
due to three interacting issues:

1. The CSP connect-src directive did not include data:, so the
   qr-code-styling library's internal XHR to convert logo data URLs to
   blobs was silently blocked. The library has no onerror handler, so the
   render promise hung forever after the container was already cleared.

2. crossOrigin: "anonymous" was unnecessarily set on imageOptions for
   data URLs, which can cause canvas taint issues.

3. The logo options used a conditional spread that omitted the image key
   when no logo was set. The library's update() deep-merges options, so
   removing the logo preserved the stale data URL and the QR stayed
   broken even after logo removal.

Closes #121
2026-05-06 21:38:12 +08:00
SnapOtter 84e62e6132 refactor: rename tool display name to "PNG Transparency Fixer" 2026-05-06 18:53:37 +08:00
SnapOtter c47ec34021 fix: show tool name instead of bundle name in install prompt, add queued state 2026-05-06 18:47:34 +08:00
SnapOtter eb3d0828cb docs: add transparency-fixer endpoint documentation 2026-05-05 23:24:22 +08:00
SnapOtter 52f355ef8f feat: register transparency-fixer in frontend tool registry 2026-05-05 23:09:15 +08:00
SnapOtter 06d6c0ef1e feat: add transparency-fixer settings component 2026-05-05 23:09:10 +08:00
SnapOtter b1959e977d fix: improve defringe quality and add defensive guards in transparency-fixer 2026-05-05 23:07:14 +08:00
SnapOtter 423711ac42 feat: add transparency-fixer API route with defringe post-processing 2026-05-05 23:03:07 +08:00
SnapOtter d46510138f fix: custom logo not showing in sidebar and silent upload failures
The desktop sidebar never rendered the custom logo because only mobile
views used the customLogo state. Added a logo section at the top of
the desktop sidebar that displays the custom logo (or the default
OtterLogo when none is set).

The upload handler used raw fetch() without checking response.ok, so
HTTP 4xx errors (e.g. file too large) were silently ignored and the UI
falsely reported success. Now checks response status and surfaces the
server error message.

Closes #125
2026-05-05 21:45:51 +08:00
SnapOtter 7ff100c4f9 docs: update tagline to "Your images never leave your home" 2026-05-05 21:12:44 +08:00
SnapOtter 1807a43f2d fix: crop tool overflows viewport with tall portrait images
The crop area flex container lacked min-h-0, preventing it from shrinking
below its content's intrinsic height. For tall images, react-image-crop's
CSS (max-height: inherit at specificity 0-1-1) overrides the Tailwind
max-h constraint, and without min-h-0 the flex item refuses to shrink,
causing viewport overflow.

Closes #122
2026-05-05 17:51:01 +08:00
SnapOtter e358634f8b fix: production CSP blocking PostHog/Sentry/Scalar and silent failure hardening
The production CSP had connect-src/script-src/font-src set to 'self' only,
silently blocking all analytics and error reporting in production while
working fine in dev (where CSP is not applied).

CSP fixes:
- Add PostHog ingest + assets origins to connect-src and script-src
- Add Sentry ingest origin to connect-src
- Add Scalar fonts origin to font-src for API docs pages
- Extract CSP construction into testable buildCsp() function

Silent failure hardening:
- Settings/features stores now set loadError flag and allow retry on
  subsequent fetch() calls instead of permanently caching failed state
- Analytics init no longer sets initialized=true before the try block,
  allowing retry on failure
- Settings dialog Tools section disables save button when settings
  failed to load, preventing accidental config wipe
- Branding logo storage moved from process.cwd() to FILES_STORAGE_PATH
  so logos persist across Docker container recreation

Test coverage:
- 16 CSP directive tests covering all external service domains
- Store retry-on-error behavior tests for settings and features stores
- Analytics init retry-after-failure test
2026-05-05 17:16:19 +08:00
SnapOtter 6a44c455a3 fix: remove printf banner from docker commands in docs
The printf welcome banner was overflowing code blocks on
docs.snapotter.com, making docker commands hard to read and copy.
2026-05-01 19:42:11 +08:00
SnapOtter 66211ed6a7 fix: resolve 20 Sentry issues and fix navbar test flakiness
Sentry fixes:
- Only send 5xx errors to Sentry (was sending 4xx rate-limit, media type errors)
- Encode non-ASCII chars in X-Output-Filename header (encodeURIComponent)
- Handle FK constraint failures gracefully in file upload, pipeline save, API keys
- Harden getDirSize against ENOENT race on readdirSync

Test fixes:
- Wrap navbar test renders in act() to flush async useEffect state updates
- Add useEffect cleanup to navbar to prevent state updates on unmounted component
- Fixes timeout when running in full test suite
2026-05-01 19:01:05 +08:00
SnapOtter ff8dcf63c7 fix: resolve 14 security, correctness, and robustness issues found during QA sweep
Security fixes:
- Add auth + ownership check to thumbnail endpoint (was unauthenticated)
- Validate ExifTool fieldsToRemove against safe tag name pattern
- Add SVG sanitization to pipeline execute and batch endpoints
- Replace basename() with sanitizeFilename() in 16 tool routes
- Escape SQL LIKE wildcards in file search to prevent pattern injection
- Improve settings HTML tag validation pattern

Bug fixes:
- Skip autoOrient for SVG inputs in pipeline (prevents misinterpretation)
- Remove double-encode in compress targetSize (was degrading quality)
- Fix bg-effects alpha value from 255 to 1.0 (Sharp expects float)
- Guard download stream error handler against headers-already-sent race
- Use O_EXCL atomic file creation for install lock (fixes TOCTOU race)
- Truncate collage file array to template image count

UX fixes:
- Accept empty JSON bodies on POST endpoints (install/uninstall)
- Custom JSON content type parser that treats empty body as {}
2026-05-01 18:11:49 +08:00
SnapOtter 84f24b8736 chore: add Discord community links across README, docs, and landing page
- README: Discord badge in header + community section link
- Docs: GitHub and Discord social links in VitePress config
- Landing: Discord in navbar, footer, open-source section, pricing copy
2026-05-01 16:13:38 +08:00
SnapOtter fa479dcee4 fix: resolve 3 pre-existing issues found during test coverage expansion
1. passport-photo 404 vs 501: add base route at /api/v1/tools/passport-photo
   that returns 501 FEATURE_NOT_INSTALLED when the AI bundle is missing,
   matching other AI tools. The /generate sub-route is Sharp-only (no
   sidecar) so it correctly skips the isToolInstalled guard.

2. AuthGuard analytics consent race: don't evaluate shouldShowConsent()
   until analyticsConfig has been fetched (guard on analyticsConfig !== null).
   Prevents redirect to /analytics-consent before config is loaded.

3. Fragile sidebar Settings selector: add openSettings(page) helper to
   E2E helpers that checks sidebar visibility with fallback to button role.
   Replace all 134 occurrences of page.locator("aside").getByText("Settings")
   across 18 test files.
2026-05-01 13:44:04 +08:00
SnapOtter d12b1c0fc6 fix: harden all AI tools against proxy timeouts and filename attacks
Convert all 9 AI tool routes (colorize, restore-photo, remove-background,
enhance-faces, blur-faces, red-eye-removal, erase-object, noise-removal,
upscale) to async 202 processing so none are vulnerable to proxy
connection timeouts.

Also fixes:
- Replace basename() with sanitizeFilename() in all AI tool routes
  (prevents double-extension attacks and adds length truncation)
- Add UUID format validation for clientJobId field
- Fix missing filename sanitization in noise-removal (was using raw
  user-supplied filename with zero sanitization)
- Remove em dash from error message in use-tool-processor
2026-05-01 00:11:03 +08:00
SnapOtter 4900d8a4fe fix: upscale times out behind Cloudflare Tunnel due to blocking HTTP request
The upscale route held the HTTP connection open for the full duration of
Python sidecar processing (30-300s). Behind proxies with connection
timeouts (Cloudflare Tunnel: 100s), this caused HTTP 524 errors.

The route now returns 202 Accepted immediately after upload validation
and processes in the background. The result (downloadUrl, sizes, etc.)
is delivered via the existing SSE progress channel. The frontend detects
the 202 and waits for the SSE completion event instead of reading the
XHR response body. A reconnect-safe completion store ensures results
survive brief SSE disconnects.

Closes #106
2026-04-30 23:43:55 +08:00
SnapOtter 42afa7c0bf fix: eagerly start AI dispatcher at boot and log actual GPU status
Replaces the misleading 'waiting for AI sidecar startup...' message that
never resolved. The dispatcher now starts during server init, and the
startup log shows the actual GPU detection result.
2026-04-30 18:49:52 +08:00
SnapOtter b00ef20667 fix: close SVG sanitization gap on upload routes and fix OCR/extension bugs
Security:
- Apply sanitizeSvg() to all file upload routes (files.ts, user-files.ts)
  preventing SSRF and script injection via SVG uploads to file library

Functional:
- Handle PaddleOCR-VL 1.5 markdown_texts output format in ocr.py
- Add empty-text fallback in OCR tier chain (ocr.ts) so higher tiers
  that return empty text fall back to the next tier automatically
- Fix SVG->PNG filename extension mismatch in tool-factory.ts so
  download endpoint serves correct Content-Type
- Report original upload size (not decoded size) in API response

Test infrastructure:
- Move Playwright auth state from test-results/ to .playwright/ to
  prevent mid-run cleanup deleting auth files
- Fix auth.setup.ts navigation race with waitForURL
- Fix gui-batch.spec.ts regex matching "Presets" instead of "reset"
- Fix pipeline-advanced.spec.ts crop bounds and resize assertions
- Broaden pipeline cleanup to include all E2E-prefixed pipelines
2026-04-30 16:11:33 +08:00
SnapOtter fc8b549d78 fix: gate captureException on user consent and fix HEIC PII scrubbing
captureException now checks isRequestOptedIn before forwarding errors
to Sentry, closing a gap where server errors leaked to an external
service even when no user had consented. The PII scrubbing regex is
also fixed: he[ic]f? failed to match .heic due to word-boundary
behavior and is replaced with hei[cf]? which correctly covers .heic,
.heif, and .hei.

Adds 88 new analytics tests across unit, integration, and e2e layers
proving PostHog/Sentry are never invoked when analytics is disabled or
users have not consented, plus full 7-day reminder lifecycle coverage.
2026-04-29 23:47:19 +08:00
SnapOtter 53343a0836 fix: wire up 7-day consent re-prompt in AuthGuard
The shouldShowConsent function in the shared package had the correct
logic for checking analyticsConsentRemindAt, but the AuthGuard never
used it. The inline check only redirected to the consent page when
both analyticsEnabled and analyticsConsentShownAt were null, which
is never true after "remind later" since shownAt gets set.

- Destructure analyticsConsentRemindAt from useAuth session
- Hydrate remindAt into the analytics store instead of hardcoding null
- Replace inline redirect check with shouldShowConsent from shared pkg
- Read analyticsConfig from the store inside AuthGuard for serverEnabled
2026-04-29 14:42:37 +08:00
SnapOtter 4d3e5e9c02 fix: defer PostHog/Sentry loading until user consents to telemetry
PostHog SDK was initialized on app mount based only on the server-level
config flag, ignoring user consent. This caused network requests to
us-assets.i.posthog.com (config.js, web-vitals.js, dead-clicks-autocapture.js)
even when the user had not opted in or had explicitly declined telemetry.

- Replace static imports of posthog-js and @sentry/react with dynamic
  import() so the SDK bundles are not downloaded until consent is granted
- Gate initAnalytics on analyticsConsent.analyticsEnabled === true,
  not just server config.enabled
- Add consent re-check after each await import() to handle revocation
  during the async load
- Add shutdownAnalytics() that calls opt_out_capturing() + reset()
  for mid-session consent revocation
- setAnalyticsConsent(false) now triggers full SDK shutdown automatically
- Rewrite analytics test suite with 44 tests covering init gating,
  shutdown lifecycle, consent toggle, race conditions, and Sentry callbacks

Closes #98
2026-04-29 14:16:38 +08:00
SnapOtter e6cb3ef91d docs: add targetSize parameter to image-to-pdf API docs 2026-04-27 22:39:57 +08:00
SnapOtter 6c1f8363bf feat(web): add target file size controls to image-to-pdf settings 2026-04-27 22:10:20 +08:00
SnapOtter 444b5d80ab feat(api): add target file size compression to image-to-pdf 2026-04-27 22:06:44 +08:00
SnapOtter fb249faeb2 fix: show queued/installing status for AI tools on main page
Main page tool cards and home page tool lists only subscribed to
server-side bundle state, which only reflects the actively downloading
feature. Queued features appeared as plain download icons instead of
showing their queued/installing status. Now subscribes to client-side
installing and queued state from the features store, matching the
settings page behavior.
2026-04-27 21:27:55 +08:00
SnapOtter 4f81b29fbc fix: AI feature install failures — missing rembg session and Fastify 415 (#102, #103)
Register custom BiRefNet-matting ONNX session in install_feature.py so
rembg.new_session("birefnet-matting") no longer raises ValueError during
on-demand installs. The session was already registered in remove_bg.py
(runtime) and download_models.py (build-time) but was missed in the
install path, causing background-removal bundle installs to always fail.

Send JSON body on install/uninstall POST requests to avoid Fastify 5's
strict content-type parser rejecting body-less POSTs with 415.

Fix error message extraction to preserve structured {"error": ...} JSON
from the Python script and filter out pthread_setaffinity_np noise.
2026-04-27 02:38:17 +08:00
SnapOtter b2a1769c8f fix: QA sweep fixes — OCR engine mapping, startup log, Playwright config
- Update OCR engine expected name from "paddleocr" to "paddleocr-v5"
  to match actual PaddleOCR PP-OCRv5 engine (eliminates spurious
  fallback warning in logs)
- Show "waiting for AI sidecar startup" instead of misleading
  "No GPU detected" when Python dispatcher hasn't reported yet
- Fix playwright.docker.config.ts testDir to ./tests/e2e-docker
  and align auth storage state path with auth.setup.ts
2026-04-27 01:13:32 +08:00
SnapOtter dee9452c48 fix: format preservation, dispatcher stability, and health reporting
Closes #17, #18, #19, #31, #32, #33, #34

Format preservation (#17, #18, #19):
- Add resolveOutputFormat to rotate, resize, text-overlay, watermark-text,
  border, replace-color, blur-faces, upscale, erase-object, restore-photo
- Alpha-aware fallback: border with corner radius/shadow and replace-color
  with makeTransparent fall back to PNG for non-alpha formats (JPEG)
- Python sidecar tools (blur-faces, upscale, erase-object) now convert
  PNG output back to input format, matching restore-photo/colorize pattern
- Upscale and erase-object default to "auto" format detection instead of PNG

Dispatcher stability (#31, #32):
- Add gc.collect() and torch.cuda.empty_cache() after each dispatcher request
- Add configurable max_requests (default 50) for periodic dispatcher restart
- Add exponential backoff to dispatcher crash recovery in bridge.ts
- Circuit breaker: 5 crashes within 60s permanently disables dispatcher
- Reset crash counter on successful dispatcher startup

Health & security (#33, #34):
- Export getDispatcherStatus() from @snapotter/ai with running/ready/failed/
  gpu/pid/consecutiveCrashes fields
- Admin health endpoint now includes full dispatcher status
- Add pip-audit job to CI workflow for Python dependency scanning
2026-04-26 03:22:26 +08:00
SnapOtter 86db131198 fix: JXL decode fallback and Playwright remote container support
Add djxl (libjxl-tools) as primary JXL decoder with ImageMagick
fallback — fixes JXL format failures on Ubuntu where stock ImageMagick
lacks a JXL delegate. Also make Playwright Docker config respect
BASE_URL env var for testing against remote containers.
2026-04-26 02:53:02 +08:00