Commit Graph
265 Commits
Author SHA1 Message Date
SnapOtterandGitHub 6e3a14ec6b fix: remove automatic third-party egress of user data + optional strict offline mode (OSM tiles, Scalar fonts, editor fonts, AI model downloads) (#422)
* fix: remove all automatic third-party egress (OSM tiles, Scalar fonts, editor Google Fonts, AI model download fallbacks)

Phone-home audit follow-up. The product no longer makes any automatic
third-party request; user-initiated click-outs stay, and production now
fails closed on missing AI models.

1. GPS leak via OSM tiles: the strip-metadata panel auto-loaded
   tile.openstreetmap.org tiles encoding the photo's GPS position. The
   Leaflet mini-map is gone; coordinates render as text plus an explicit
   View on map link (openstreetmap.org, opens on click only). Removed
   tile.openstreetmap.org from the CSP img-src, dropped the leaflet
   dependency, added the viewOnMap i18n key to all 21 locales.

2. Scalar docs fonts: /api/docs loaded Inter and JetBrains Mono from
   fonts.scalar.com. Scalar now renders with withDefaultFonts: false and
   both --scalar-font and --scalar-font-code pinned to system stacks;
   fonts.scalar.com removed from the docs CSP font-src. Verified by
   injecting GET /api/docs/: config carries withDefaultFonts false and
   the served page has no fonts.scalar.com reference.

3. Editor Google Fonts: the editor font picker built
   fonts.googleapis.com stylesheet URLs for 25 web fonts the served CSP
   already blocked. The remote loading path is deleted; the picker now
   offers system fonts only, with a SELF_HOSTED_FONTS seam (FontFace API,
   same origin) for bundling fonts later. Unknown families saved in old
   documents fall back to the browser default.

4. Python sidecar fails closed on model downloads: new
   packages/ai/python/offline_guard.py gates every runtime download
   fallback (inpaint, outpaint, restore, noise_removal, detect_faces,
   enhance_faces, face_landmarks, red_eye_removal, remove_bg, ocr,
   transcribe, upscale) behind SNAPOTTER_ALLOW_MODEL_DOWNLOAD=1 with an
   actionable error. Bundled models keep working untouched.

5. OCR and transcription library-internal downloads: unbundled PaddleOCR
   language and detection fallbacks now raise the guard error naming the
   language instead of resolving models over the network; faster-whisper
   gets local_files_only when downloads are off.

6. GFPGAN and CodeFormer cwd-relative weights: facexlib and
   codeformer-pip resolve helper weights relative to the process cwd and
   fetch them from GitHub when absent. They are now symlinked from the
   installed bundle files under MODELS_PATH/gfpgan/facelib before the
   libraries load, failing closed when unresolvable.

Defense in depth: HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 are set in
the runtime image and in the sidecar spawn env; install_feature.py lifts
them for user-initiated bundle installs and restores them afterwards
(it can run in-process inside the dispatcher). SNAPOTTER_ALLOW_MODEL_DOWNLOAD
is documented in .env.example, default off.

Validation: typecheck 9/9 workspaces, Biome clean on touched files,
5178 unit tests pass, py_compile on all touched scripts, guard behavior
exercised in both dispatcher exec and per-request import modes, zero
remaining runtime references to the three hosts. Docker build and live
AI inference need post-merge verification on the GPU host.

Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7

* fix: allow AI model downloads by default, make strict offline mode opt-in

Product call: ease of use first. The download gating from the previous
commit inverts its default: runtime model fetches (public model weights
only, never user data) are allowed out of the box so AI tools self-heal,
and SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0 becomes the explicit strict offline
mode for airgapped deployments, where every fallback raises the
actionable error instead of fetching.

Changes: offline_guard blocks only on an explicit 0/false; the
unconditional HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE image ENV is removed
and bridge.ts sets those flags for the sidecar only in strict mode;
.env.example documents the new default; install_feature's lift/restore
stays. All bundled-path preferences, pre-existence checks, and symlink
pre-placement remain, so installed bundles never trigger a download.
The OSM, Scalar font, and editor font fixes are unchanged.

Validation rerun: typecheck 9/9, Biome clean on touched files, 5178
unit tests pass, py_compile on touched scripts, guard behavior verified
for unset/1 (allowed) and 0/false (blocked with the new message).

Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7
2026-07-04 05:46:52 +00:00
SnapOtterandGitHub b4375e558d fix: harden install queue/dispatcher lifecycle and repair review-sweep regressions (#395)
Fixes 15 defects found by a max-effort multi-agent review of the last 6
merged PRs (#388, #390, #391, #392, #393, #394), all adversarially
verified before fixing.

Install queue + dispatcher (the serious cluster):

- features.ts: finalize the installer child exactly once. A failed spawn
  fires both "error" and "close", and the second event released the file
  lock and active slot that pump() had just handed to the next queued
  bundle, letting two pip processes write the same venv concurrently.
  Outcome recording now happens before pump() so the next bundle's first
  progress frame cannot race the previous install's bookkeeping.
- feature-status.ts: keep failed-install errors in a per-bundle map
  instead of the single progress slot. With the queue auto-starting the
  next install, the slot was overwritten within seconds and a failed
  install vanished without ever surfacing to GET /features.
- bridge.ts: scope child lifecycle per process (stopped-children set +
  request generation tags) instead of an instance-wide shuttingDown flag
  that the next spawn reset. A stale SIGTERMed child's late close event
  could record a phantom crash (5 of which permanently disable the
  dispatcher), null out the freshly spawned child, and reject the new
  child's pending requests. The request-timeout kill path still counts
  as a real crash.
- install_feature.py: the pre-write disk re-check measured ai_dir's
  filesystem even when budgeting the cross-filesystem copy that lands on
  the venv's disk; now each budget is checked against the filesystem the
  bytes actually land on, so ENOSPC cannot strike mid-write and leave
  site-packages half overwritten.

Behavior regressions:

- embed-subtitles: preserve pre-existing subtitle tracks (0:s?) and MKV
  attachments (0:t?) that the -map 0:v:0/0:a? rewrite silently dropped;
  data streams stay unmapped on purpose (the actual MPEG remux fix). The
  new subtitle maps first so the language tag hits the right stream.
- usage-survey-overlay: fail closed when the settings fetch fails; the
  fail-open path rendered the blocking survey against an unhealthy API
  and soft-locked admins, the lock-out class #392 fixed.
- features-store: queued bundles poll instead of each holding an SSE
  connection (Install All could pin 7 EventSources and exhaust the
  browser's 6-per-origin HTTP/1.1 limit, hanging the whole app);
  listenToProgress closes any prior stream and stops any poll before
  subscribing; installAll skips bundles already installing or queued.

Contracts, tests, i18n:

- openapi.yaml: add "queued" to the features status enum and document
  downloadBytes/installedBytes (Schemathesis conformance).
- feature-lifecycle e2e: queue transcription (~0.5 GB) instead of ocr
  (~6 GB) and give the test a budget that covers both install drains
  (the stacked waits exceeded the old 900s timeout).
- docker-compose.qa.yml: parameterize the host port (QA_APP_PORT) so
  QA_PROJECT_NAME concurrent stacks can actually bind.
- compare + watermark-image: restore per-input error attribution
  ("Invalid first/second image", "Invalid watermark image") lost in the
  shared-handler migration.
- ai-features-section: the "{size} on disk" suffix now goes through
  i18n; key added to all 21 locales.
- watermark-image + content-aware-resize: migrate to the shared
  inputHandlerFor("image") chain like compare/vectorize/compose, fixing
  drift in the inline copies (no SVG sanitize, no RAW extension hint,
  no AVIF probe).

Verified: typecheck across 9 workspaces, Biome clean on all changed
files, 584 targeted unit tests and 249 integration tests green
(including real-ffmpeg embed-subtitles runs). One unit test updated to
the new poll-while-queued contract with a single-EventSource assertion.

Claude-Session: https://claude.ai/code/session_017mR1HiHaf3a1BmUtrHX4j3
2026-07-03 13:47:15 +08:00
SnapOtterandGitHub b37faed95f fix: QA sweep - tool routes, security, i18n, a11y, + AI bundle install hardening (#393)
* fix(api): correct format/filename/container handling across tool routes

Found during a comprehensive QA sweep exercising every tool against its
full accepted-format matrix:

- watermark-image, compose: preserve the requested output format and a
  matching download filename/extension instead of always emitting the
  source format
- compose: crop oversized overlays to the visible base area instead of
  crashing Sharp's composite, and reject only overlays fully outside the
  base image instead of any oversized one
- compare, vectorize: switch to the shared image input handler so
  filenames and formats like .svgz/.tga/RAW survive validation instead
  of being rejected pre-processing
- tool-factory, images-to-video: normalize frames through Sharp before
  handing them to FFmpeg, fixing GIF/AVIF/RAW image-to-video jobs that
  previously failed or hung
- media-tool, replace-audio, embed-subtitles: fix legacy container
  MIME/codec handling for MPEG sources and subtitle remux cases
- files: expand download MIME mapping for text/data/document/video/audio
  outputs that were falling back to a generic content type
- convert-document/presentation/spreadsheet: same-format conversions now
  return the original validated file instead of erroring or producing
  corrupt tiny output

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(web): dropzone a11y, stale localStorage getter, dead code

- dropzone: stop making the whole drop-zone section clickable/focusable.
  A section acting as an interactive element around a real upload button
  is a nested-interactive-element anti-pattern that confuses screen
  readers; drag-and-drop doesn't need focus semantics, only the button
  fallback does. Keeps that button semantic and keyboard-reachable.
  Updates the two e2e call sites that clicked the section directly.
- api, use-auth: read through window.localStorage via the existing API
  storage helper instead of the bare global, which resolves to Node's
  experimental localStorage getter under Vitest and threw
- find-duplicates-settings, info-settings, login-page: remove dead code
  (unused zip-download handler, a stale mount-only effect dependency
  that left cached info stuck at reused indices, an unused response
  variable)

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(i18n): pt-BR, zh-CN, zh-TW were silently falling back to English

The locale loader looked up dynamic-import exports by the raw locale
code (mod["pt-BR"], mod["zh-CN"], mod["zh-TW"]), but those three modules
export camelCased bindings (ptBR, zhCN, zhTW) since identifiers can't
contain hyphens. The lookup returned undefined and every consumer
silently fell back to English for these three locales. Replaces the
generic lookup with explicit per-locale loaders so the mapping can't
drift out of sync again.

Also updates the dropzone helper copy across all 21 locales to match
the drag-only dropzone wording from the previous commit.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(docs): clear build warnings in the VitePress site

- config.mts: add an onwarn handler for the @vueuse INVALID_ANNOTATION
  warnings emitted during the docs build
- deployment.md: the caddyfile code fence language isn't a shiki grammar
  VitePress ships with, so it warned on every build; use txt instead

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* test(qa): update QA harness for the drag-only dropzone and regen metadata

- api-sweep, qa-helpers, verify-ai: add JSON-body tools, multi-input
  secondary fixtures, async polling for slow valid jobs, 501
  FEATURE_NOT_INSTALLED skip handling, and safer per-tool settings
- input-preview, pipeline-ui specs: update upload flow for the
  drag-only dropzone surface
- add tests/fixtures/data/valid/chart.json, a valid chart fixture the
  updated helpers route to
- regenerate tools-meta.json against current TOOLS[]

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(security): close a login timing side-channel, harden zip-slip tests

Found during a black-box security sweep of the real auth-enabled
production container: a nonexistent username returned 401 in ~3-10ms,
while a wrong password for a real user took ~35-42ms, because scrypt
verification only ran when a user row existed. That timing gap lets an
attacker enumerate valid usernames without ever guessing a password.
Now runs verification against a cached dummy hash on the unknown-user
path too, so both cases cost the same regardless of outcome.

extract-zip already had a relative-traversal regression test
(../evil.txt), but its absolute-path rejection branches
(name.startsWith("/") / startsWith("\\")) had none. Added the three
missing cases: deep relative traversal, absolute Unix path, and
Windows-style absolute path.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* test(qa): add UI-driven AI bundle install scripts

QA_PROMPT.md's Phase 2 requires installing AI models the way a user
does -- through the UI, on demand from HuggingFace -- and treats the
curl-based admin install endpoint as fallback-only. Nothing in the
harness actually drove that flow; tests/qa/seed-ai-models.sh installs
via docker exec + pip, which is further from a real user than even the
API fallback.

install-ai-bundles-ui.mts logs in, opens Settings > AI Features,
screenshots the pre-install state, clicks Install All, and screenshots
progress -- then exits, since installs continue server-side once
triggered. verify-ai-install-complete.mts polls bundle status,
screenshots the completed state, and runs one real tool per installed
bundle to prove the freshly-downloaded model actually executes.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(qa): correct the apiToolPath import in the AI verify script

Dynamic import of the package name failed under tsx's module resolution
from apps/api's node_modules context; use the same relative-path import
api-sweep.mts already uses successfully.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(web): correct AI bundle size estimates shown before install

Measured real downloads during GPU-node QA verification: photo-restoration
pulls ~4.4GB (was advertised as 800MB-1GB, off by 4-5x) and ocr pulls
~5.5GB (was advertised as 3-4GB). Both estimates only accounted for model
weights, not the pip dependencies (torch/paddle) that come down with them.
Updated to reflect actual total download size, since that's what a user
deciding whether they have the disk/bandwidth actually needs to know.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(web): make desktop Settings reachable when auth is disabled

AvatarDropdown (the only desktop entry point to Settings) was gated
behind `!isMobile && authEnabled`. With AUTH_ENABLED=false the synthetic
anonymous admin user should have full Settings access per how auth.ts
documents this mode -- and the mobile bottom nav already worked this way,
showing Settings unconditionally. Desktop just had a stray extra gate the
component doesn't need: AvatarDropdown already resolves its own username
internally (falling back to "admin") and reads authEnabled itself where
it actually matters (hiding the Logout button). Removed the outer gate;
verified end-to-end against a fresh AUTH_ENABLED=false instance -- avatar
now renders, Settings opens, shows the anonymous/Admin identity correctly.

Also documents (not changes) a related finding in install_feature.py:
detect_arch() always resolves amd64 hosts to the GPU-bundled archive
variant regardless of actual GPU presence, since no CPU-only amd64
archive is published to the bundle repo yet. Left as a code comment
rather than a behavior change, since requesting an unpublished archive
key would hard-fail installs entirely -- worse than the current
oversized-but-working download. Full detail in the QA report.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(ai): stop logging expected dispatcher reloads as crashes

After each AI bundle install the Python dispatcher reloads because the
venv changed, and after every app shutdown it's SIGTERMed. Both took the
close handler's `code !== 0` branch (SIGTERM makes the exit code null),
so they were counted as crashes -- producing an alarming "crash" line in
the logs and a pointless ~1s recovery backoff after each of 7 installs.
A `stopping` flag set in shutdown() lets the close handler tell an
intentional stop apart from a real crash. The request-timeout kill path
deliberately does not set it, so a genuinely hung script still records a
crash and the 5-in-60s permanent-disable threshold is untouched.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(api): return a clean message when content-aware resize times out

Carving a very high-resolution image down to a tiny target could exceed
the caire subprocess timeout, and the raw error forwarded to the user was
caire's terminal output -- ANSI color codes and progress-spinner control
characters -- instead of anything actionable. Now: the timeout path
throws a clear "timed out; try a smaller image or larger target" message
(keeping the raw stderr as `cause` for server logs); friendlyError()
strips ANSI/control chars centrally so any subprocess dump surfaced
through the shared sanitizer is plain text; and the content-aware-resize
route (a custom route that bypassed the sanitizer) now routes its error
paths through friendlyError like every other tool.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(ai): stop bundle installs from exhausting host disk

Installing an AI bundle on a tight-disk host could push the root
filesystem to zero bytes free after the preflight check had already
passed. Two root causes:

- move_tree used copytree+rmtree, so during the move the extracted
  payload existed in both staging and the venv at once -- a full
  transient doubling on disk. Rewrote it to rename entries (a cheap
  metadata op on the same filesystem, no copy), falling back to a copy
  only across filesystems.
- the preflight budget used the manifest's extractedSize verbatim, which
  is 0 for several archives, collapsing the estimate to just the
  compressed size. Added a conservative fallback (3x compressed) so a
  missing value can't under-reserve.

Also added a real-on-disk re-check immediately before the first
destructive venv write (measuring the actual extracted payload and
whether the move needs extra space for a cross-filesystem copy), which
also now covers the offline-import path that previously skipped the disk
check entirely; wrapped the moves so an out-of-space failure returns a
clean actionable error instead of a traceback; and made the disk check
resolve the nearest existing ancestor so it never throws on a
not-yet-created venv path.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* feat(web): show the real per-arch AI bundle download size

The bundle cards and install prompt showed a hardcoded, architecture-blind
estimatedSize string. That's misleading: amd64 hosts always pull the
CUDA-inclusive archive (there's no CPU-only amd64 variant published), so a
bundle labelled "1-2 GB" can actually download several times that, while
arm64 pulls a much smaller archive for the same label. The manifest
already carries the real per-arch compressedSize (and extractedSize where
measured), so surface those: a new optional downloadBytes/installedBytes
on FeatureBundleState, populated in getFeatureStates() for this host's
arch (resolver mirrors install_feature.py detect_arch), shown by the UI
when present with estimatedSize kept as the fallback label. Also nudged
upscale-enhance's fallback string (4-5 -> 5-6 GB) to match its real
compressed size, consistent with the earlier photo-restoration/ocr fixes.

Fields are optional so demo/mock and existing tests stay compiling; the
manifest's extractedSize is 0 for a few archives, which now surfaces as
null rather than a bogus 0.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(web): move the AI install queue to the server so it survives tab close

Installing multiple bundles could silently lose all but the first. The
server rejected a concurrent install with 409, so the client worked
around it by queueing the rest in browser-local state and only POSTing
each once it saw the previous finish. A single POSTed install is durable
(the installer child is detached from the request), but a queued one had
zero server footprint -- close the tab mid-queue and those installs
vanished with no error, while the UI still showed them "Queued". The
client "mutex" didn't even serialize: the queued bundles' local waits all
resolved at once and raced into concurrent POSTs that 409'd each other.

Now the queue lives on the server (a small in-memory FIFO leaf module).
The install endpoint enqueues instead of 409-ing and returns
202 {jobId, queued}; a pump starts the next bundle when the current one's
child exits (and after an offline import releases the lock), all behind
the existing venv + file locks, which are unchanged. The client just
POSTs every bundle immediately and reflects the server-reported
queued/installing status; Install All fires all POSTs and lets the server
serialize them, keeping the one-shot retry-on-failure. Adds "queued" to
FeatureStatus (the bundle card already rendered that state) and surfaces
it from getFeatureStates. In-memory is deliberate: it matches the
existing contract (survives a tab close, not a server restart, which
already clears the lock on boot).

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG

* fix(qa): don't log env-derived credentials in the AI-install script

CodeQL flagged clear-text logging of sensitive information: the login
status line interpolated the QA base URL and username (both read from
the process environment) into a console.log. Replaced with a static
message. QA helper only, but it's a real hygiene issue and cleared the
high-severity code-scanning alert on the PR.

Claude-Session: https://claude.ai/code/session_019fpSXhLGLXWwfyZY2tWhLG
2026-07-03 09:54:02 +08:00
SnapOtterandGitHub ca076f91fd fix: critical first-login soft-lock in usage survey overlay (#392)
* fix: prevent UsageSurveyOverlay from soft-locking the first-login password-change flow

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* fix: prevent double feedback submission when the settings write fails

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* refactor: consolidate feedback enums into packages/shared as a single source of truth

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* feat: add ARIA semantics, dismiss-button guard, and shared auth-route list to UsageSurveyOverlay

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* test: cover the submit-failure retry path and a persona-only minimal payload

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp
2026-07-02 18:37:12 +08:00
SnapOtterandGitHub a0d1c70172 feat: add usage onboarding survey overlay (#388)
* feat: add usage-survey feedback types and gating function

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* feat: add onboarding usage-survey i18n strings to all locales

Relabels three ambiguous feedback.usageTypes values (personal/team_internal/
business_workflow) and adds a new onboarding namespace (4 keys) across the
reference locale and all 20 translations, so the tree compiles at every
commit instead of only after both locale groups land.

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* feat: add UsageSurveyOverlay component

* feat: mount UsageSurveyOverlay inside AuthGuard

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* fix: use text-start instead of text-left for RTL support in UsageSurveyOverlay

* refactor: drop redundant usage-type field from the admin feedback dialog

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* feat: accept onboarding source and survey id in the feedback route

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* test: cover the onboarding source in the feedback route integration test

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* chore: remove orphaned usageTypeLabel i18n key

* refactor: derive feedback source/survey_id enums from a single source of truth

* perf: skip the settings fetch in UsageSurveyOverlay for non-admin users
2026-07-02 00:02:24 +08:00
SnapOtterandGitHub c68297d5a4 feat: request a tool when home search finds nothing (#385)
Adds a prefilled 'Request a tool' affordance to the home search empty state and beneath weak results. Opens the in-app feedback dialog with a new search_miss source and a structured search_query when analytics is on; links to a prefilled GitHub Discussions (Ideas) post when off, so a request is never silently dropped. Reuses the existing feedback pipe, dialog, and analytics gate; no new storage. i18n across all 21 locales.
2026-07-01 18:50:55 +08:00
SnapOtter 649e65b035 feat: add PostHog customer feedback 2026-06-29 18:16:33 +08:00
SnapOtterandGitHub 0cdd560ac4 feat: add Sign PDF tool (draw/type/upload signatures, place on a PDF) (#370)
Draw, type, or upload a signature and place resizable/rotatable copies across PDF pages; output flattened server-side with PyMuPDF. Visual electronic signature, not cryptographic. New interactive-sign display mode (pdf.js + Konva) and a custom docs-pool route.
2026-06-29 11:01:25 +08:00
SnapOtterandGitHub 9a556d4851 chore(i18n): complete the 200+ tools sweep across the remaining 19 locales (#366)
PR #364 only changed en/nl; the appDescription and rotating-phrase tool count was still '240' (translated) in the other 19 locales (ar, de, es, fr, hi, id, it, ja, ko, pl, pt-BR, ru, sv, th, tr, uk, vi, zh-CN, zh-TW). Replaces the number in each; surrounding translations untouched. My earlier English-only grep missed these.
2026-06-28 22:09:56 +08:00
SnapOtterandGitHub 19b515dc5f chore: use "200+ tools" for the tool-count claim across public surfaces (#364)
* fix(landing): correct PDF tool count to 28 in alternatives copy

The pdf section has 28 tools (section.test.ts asserts bySection('pdf')=28). PR #363 corrected the docs breakdown but the alternatives pages still said 40 PDF tools (the document-modality count, not the pdf section) with 200 for the rest. Update to 28 PDF tools and 212 for the non-PDF remainder.

* chore: use "200+ tools" for the tool-count claim across public surfaces

Replaces the exact '240 tools' count (which drifts as tools are added) with the stable '200+ tools' on README, the Docker Hub overview, landing pages, the docs site (meta, homepage, search), the API self-description, the demo OG tag, llms.txt, package.json, the branding readme, and the en/nl app strings. Per-modality breakdown tables stay exact. Leaves the architecture doc's technical 'tool routes' figure, an internal vitest comment, and a QA report line unchanged. Updates the two tests that assert the docs strings.
2026-06-28 21:49:14 +08:00
SnapOtterandGitHub 63a03d26f2 feat: pipeline templates, analytics opt-out, 83 conversion presets, positioning + e2e modernization
Lands five integrated branches: pipeline templates (#355), analytics opt-out (#354), 83 conversion presets bringing the catalog to 240 tools (#356), self-hosted positioning (#353), and e2e modernization (#351).

Integration fixes: aligned stale web analytics tests with the opt-out/allow-list model, closed 3 CodeQL incomplete-sanitization alerts in the i18n generator, resolved settings/index/docs/format-matrix conflicts, and corrected tool counts to 240.
2026-06-28 18:57:53 +08:00
SnapOtterandGitHub 5d36ac06d8 feat(analytics): build-time bake + telemetry depth (#336)
Bake PostHog + Sentry into the published Docker image (SNAPOTTER_ANALYTICS
build arg, codegen script). Delete entire consent system. Move event emission
to BullMQ worker. Add cross-tier identity stitching, Sentry performance
tracing on both tiers, frontend funnel events. Fix stateful regex bug.

86 files changed, 1593 insertions(+), 3747 deletions(-)
2026-06-24 11:05:39 +08:00
SnapOtterandGitHub a53038ed96 feat(automate): make the pipeline builder fully multi-modal (#335)
* feat(shared): add outputModality to Tool metadata for crossing tools

* feat(shared): add modalityForExtension, toolInputModality, toolOutputModality

* fix(pipeline): route finalize and parent jobs to the pipeline's modality pool

* feat(automate): add ConvertAudioControls pipeline step (exemplar)

* feat(automate): add video tool settings controls to pipelines

* feat(automate): add audio tool settings controls to pipelines

* feat(automate): add document tool settings controls to pipelines

* feat(automate): add chart-maker settings control to pipelines

* feat(automate): warn on modality-incompatible pipeline steps

* feat(automate): add single-file download button for pipeline results

* refactor(automate): modality-aware icons, nav handler rename, mobile size bar

* test(pipeline): cover audio, document, file, and cross-modality chains

* i18n(automate): translate the modality-warning tooltip

* test(pipeline): gate media-pool routing assertion on ffmpeg availability
2026-06-24 00:21:41 +08:00
SnapOtterandGitHub ffbf4f9e10 feat(i18n): raise translation coverage across all 20 locales (#332)
Close genuinely-untranslated UI strings and fix orthography/diacritic
errors across every non-English locale, while keeping legitimate cognates,
loanwords, and technical tokens (OIDC, SAML, PDF, YAML, brand names) as-is.

- Untranslated strings closed: 225 (976 -> 751 remaining; the remainder are
  legit cognates / brand / format tokens that natives keep in English)
- Orthography: Italian 52 accent fixes (verb forms, a/e/i finals), Vietnamese
  41 strings with restored tone marks, Turkish 35, Polish 42, Dutch 17 (trema),
  plus smaller passes in es/fr/sv/pt-BR
- Biggest gap wins: Indonesian 102 -> 72; every CJK/Cyrillic/Arabic/Indic
  locale roughly halved its English leftovers

Cross-locale key parity preserved exactly; all placeholders intact.

Verified: pnpm typecheck, i18n parity/locale/tool-i18n tests (43/43),
placeholder-integrity check (0 mismatches), Biome.
2026-06-23 00:10:13 +08:00
SnapOtterandGitHub 32c1192d63 fix(passport-photo): require the face-detection bundle, not just background-removal (#329)
* fix(passport-photo): require the face-detection bundle, not just background-removal

Passport Photo runs face-landmark detection (face_landmarks.py, gated to the
face-detection bundle) before background removal (background-removal bundle),
but it was only declared under and guarded against background-removal. A user
who installed only Background Removal passed every JS-side check, then hit a
late "feature_not_installed" from the Python dispatcher gate when the analyze
step ran face landmarks, and the UI never told them Face Detection was needed.

- shared: add TOOL_EXTRA_BUNDLES + getRequiredBundlesForTool so a tool can
  declare more than one required bundle (passport-photo needs background-removal
  and face-detection). enablesTools is untouched, so the one-tool-per-bundle
  invariant still holds.
- api: isToolInstalled() now checks every required bundle; add
  getFirstMissingBundleForTool() so the analyze and base routes, pipeline (both
  guards) and batch report the bundle the user actually still needs.
- web: the proactive install prompt (tool-page) and features-store treat a tool
  as installed only when all required bundles are present, and point the prompt
  at the first missing one (sequential install, no new UI).

Refs #327

* test(passport-photo): deterministic integration coverage for the two-bundle guard

Boots the real API with an isolated DATA_DIR and controls installed.json to
prove the HTTP route behavior end-to-end:
- nothing installed -> 501 naming background-removal
- only background-removal installed -> 501 naming face-detection (issue #327)
- both installed -> guard passes (not 501)
- base route reports face-detection too

Refs #327
2026-06-22 23:31:18 +08:00
SnapOtterandGitHub 95d100c20b feat(web): in-canvas zoom & pan for the object eraser and split tools (#320)
* feat(web): add pure zoom/pan math module with unit tests

* feat(i18n): add a11y.pan key across all locales (English, matching adjacent zoom labels)

* feat(web): add useZoomPan hook (state + gestures over pure math)

* feat(web): add ZoomToolbar component

* feat(web): zoom & pan in the object eraser canvas

* feat(web): zoom & pan in the split tool preview

* fix(web): synchronous pan-mode refs so drag-pan is race-free under fast input

* test(e2e): zoom & pan acceptance (split always-on, eraser bundle-gated)
2026-06-22 20:50:28 +08:00
SnapOtterandGitHub c4ce5f96df fix(i18n): translate Swedish (sv) UI strings (#310)
Translate the ~950 previously-untranslated Swedish UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 19:39:19 +08:00
SnapOtterandGitHub 4de24b8c9e fix(i18n): translate German (de) UI strings (#308)
Translate the ~950 previously-untranslated German UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 19:39:15 +08:00
SnapOtterandGitHub 3900350ad5 fix(i18n): translate Brazilian Portuguese (pt-BR) UI strings (#307)
Translate the ~950 previously-untranslated Brazilian Portuguese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 19:39:10 +08:00
SnapOtterandGitHub fe5c7c8c86 fix(i18n): translate French (fr) UI strings (#306)
Translate the ~950 previously-untranslated French UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 19:39:05 +08:00
SnapOtterandGitHub b3ff8accec fix(i18n): translate Spanish (es) UI strings (#301)
Translate the ~950 previously-untranslated Spanish UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 19:39:00 +08:00
SnapOtterandGitHub 29fcc87708 fix(i18n): restore accents in Italian loading-message arrays (#322)
The auth.rotatingPhrases and features.progressMessages arrays were skipped by
the main Italian pass (#298) and still had a few missing accents (e.g. verb
e -> è). Diacritic-only fix; wording, placeholders, and key parity unchanged.
2026-06-22 19:38:54 +08:00
SnapOtterandGitHub 5a6368db89 fix(i18n): translate Thai (th) UI strings (#319)
Translate the ~950 previously-untranslated Thai UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:44:12 +08:00
SnapOtterandGitHub 8a8a4f99a4 fix(i18n): translate Indonesian (id) UI strings (#318)
Translate the ~950 previously-untranslated Indonesian UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:44:08 +08:00
SnapOtterandGitHub 470272169d fix(i18n): translate Vietnamese (vi) UI strings (#317)
Translate the ~950 previously-untranslated Vietnamese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:44:04 +08:00
SnapOtterandGitHub 00c89e4ae9 fix(i18n): translate Hindi (hi) UI strings (#316)
Translate the ~950 previously-untranslated Hindi UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:44:01 +08:00
SnapOtterandGitHub 8c1c2c310e fix(i18n): translate Turkish (tr) UI strings (#315)
Translate the ~950 previously-untranslated Turkish UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:56 +08:00
SnapOtterandGitHub ab37505e68 fix(i18n): translate Arabic (ar) UI strings (#314)
Translate the ~950 previously-untranslated Arabic UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:52 +08:00
SnapOtterandGitHub 6ee1b0f7a1 fix(i18n): translate Ukrainian (uk) UI strings (#313)
Translate the ~950 previously-untranslated Ukrainian UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:49 +08:00
SnapOtterandGitHub 7ce9c64b50 fix(i18n): translate Polish (pl) UI strings (#312)
Translate the ~950 previously-untranslated Polish UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:45 +08:00
SnapOtterandGitHub eb9a14a226 fix(i18n): translate Russian (ru) UI strings (#311)
Translate the ~950 previously-untranslated Russian UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:41 +08:00
SnapOtterandGitHub d036ff6d44 fix(i18n): translate Dutch (nl) UI strings (#309)
Translate the ~950 previously-untranslated Dutch UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:37 +08:00
SnapOtterandGitHub e7b412fc09 fix(i18n): translate Korean (ko) UI strings (#305)
Translate the ~950 previously-untranslated Korean UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:33 +08:00
SnapOtterandGitHub d8b037c8c6 fix(i18n): translate Japanese (ja) UI strings (#304)
Translate the ~950 previously-untranslated Japanese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:30 +08:00
SnapOtterandGitHub de4d2ed166 fix(i18n): translate Traditional Chinese (zh-TW) UI strings (#303)
Translate the ~950 previously-untranslated Traditional Chinese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:26 +08:00
SnapOtterandGitHub 26d8a0685c fix(i18n): translate Simplified Chinese (zh-CN) UI strings (#302)
Translate the ~950 previously-untranslated Simplified Chinese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.

Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
2026-06-22 16:43:22 +08:00
SnapOtterandGitHub c0a8b36dcb fix(i18n): complete and correct Italian translation (#231) (#298)
Around 35% of Italian strings (903 of 2583) were still English while the
file silently typechecked (key parity was already correct). This translates
them and fixes quality issues in the existing Italian:

- Translate untranslated tool names, descriptions, categories and UI labels;
  keep legitimate English terms (formats, URL, Team, Pipeline, device presets)
- Fix accent errors (qualita->qualità, piu->più, Si e->Si è, and similar)
- Restore the {size} placeholder dropped from settings.aiFeatures.diskUsage
- Make role labels consistent Italian: Utente / Editore / Amministratore
- Normalize stray curly apostrophes to the file's straight-quote convention

Based on the Italian translation contributed by @albanobattistella (the issue
author), reconciled against the current en.ts (e.g. the Data->Files rename)
and corrected for accents and a structural error in the source.

Closes #231
2026-06-22 16:43:18 +08:00
SnapOtter 71fefc05b0 feat(modality): rename "file" modality label "Data" -> "Files"
The fifth user-facing group is now Image, Video, Audio, PDF, Files
(internal modality id stays "file"; section.ts "files" was already
"Files"). Updates modality.ts label + comment, all 21 i18n locales
(categories.data "Data Files"->"Files", modalities.documentsAndFiles
"PDF & Data"->"PDF & Files", dead homePage.data), landing cards/hero
search/tools filter, docs headings, and e2e modality-tab assertions
(/^Data/ -> /^Files/, which had been failing).
2026-06-21 02:45:56 +08:00
SnapOtter 5ffa1d55ea Merge branch 'worktree-test+suite-overhaul-and-real-fixtures' into chore/consolidate-v2.0.0
# Conflicts:
#	tests/integration/generated/settings-matrix.test.ts
#	tests/integration/platform/api.test.ts
#	tests/integration/platform/concurrent.test.ts
#	tests/integration/platform/factory-multi-input.test.ts
#	tests/integration/security/adversarial-comprehensive.test.ts
#	tests/integration/security/adversarial-coverage-gaps.test.ts
#	tests/integration/security/adversarial-extended.test.ts
#	tests/integration/security/adversarial-final-gaps.test.ts
#	tests/integration/security/adversarial-matrix.test.ts
#	tests/integration/security/adversarial-security.test.ts
#	tests/integration/security/adversarial.test.ts
#	tests/integration/tools/image/color-adjustments.test.ts
2026-06-21 02:18:53 +08:00
SnapOtter 8301676e13 feat(web): group home grid, tabs, and breadcrumb by section 2026-06-20 12:51:41 +08:00
SnapOtter 9ca901f8d5 feat(shared): add apiToolPath() and section-prefixed routes; drop MODALITY_URL_SLUG 2026-06-20 11:05:44 +08:00
SnapOtter 790747188a feat(shared): add Section concept and toolSection() partition 2026-06-20 10:55:56 +08:00
SnapOtter f950dac792 test: add launch-readiness guards and fix zh-CN/pt-BR i18n fallback (phase 4a)
Four launch gates for v2.0.0:

1. Catalog integrity (catalog-integrity.test.ts): asserts every TOOLS entry
   is fully wired end to end (API route + frontend registry + display mode +
   process fn or REGISTRY_EXEMPT). Count checked dynamically against
   TOOLS.length. All 157 tools pass.

2. i18n cross-locale parity (i18n-parity.test.ts): asserts every locale in
   SUPPORTED_LOCALES has the same key set as en.ts. Found and fixed a real
   bug: zh-CN and pt-BR exported only a camelCase named export (zhCN, ptBR)
   with no default export, so loadTranslations silently fell back to English
   for Chinese Simplified and Brazilian Portuguese users. Fixed by adding
   export default to both files. All 20 non-en locales now pass parity.

3. Cross-modality smoke (cross-modality-smoke.test.ts): one fast tool per
   modality (rotate/image, mute-video/video, convert-audio/audio,
   rotate-pdf/document, csv-json/data) plus an auth gate. Tools needing
   ffmpeg or qpdf are gated with skipIf. Ship/no-ship signal.

4. Migration launch gate: extended migrate-from-sqlite.test.ts with a
   representative 1.x SQLite database (3 users, 3 teams, 3 settings,
   2 roles, 2 sessions, 2 API keys, 2 pipelines, 4 jobs, 4 audit entries,
   4 user files) covering boolean/timestamp/JSON/NULL type conversions,
   column remapping (input_files->input_refs, progress real->jsonb), and
   multi-row round-trip verification. 9 new test cases.

Parity: 13260 passed, 0 dropped.
2026-06-20 02:38:30 +08:00
SnapOtter 620552569e feat!: SnapOtter 2.0.0
Bump all workspace package versions and APP_VERSION to 2.0.0, marking the
official 2.0 release. Removes the stale 1.x .release-notes.md artifact
(semantic-release regenerates release notes). The 2.0/multimodality docs
and rebrand already landed on main via #254 and #261, so this carries only
the version designation forward from the rebrand branch.

BREAKING CHANGE: SnapOtter 2.0 - the platform re-architecture (Postgres 17 +
Redis 8 + BullMQ durable jobs, 157 tools across five modalities) is the 2.0
release line, replacing the 1.x SQLite single-container architecture.
2026-06-17 14:33:09 +08:00
SnapOtterandGitHub 17726ae59d docs: multi-modality rebrand, 2.0 architecture accuracy, and full OpenAPI coverage (#254)
* docs: rebrand from image-only to multi-modality across docs and metadata

SnapOtter expanded from image-only to 157 tools across 5 modalities
(image, video, audio, document/PDF, data). Update all product-level
copy, metadata, and i18n that still framed it as an image-only tool.

- README, package.json, root llms.txt: multi-modality framing, 157 tools
- OpenAPI info + tags, generated /llms.txt tagline (docs.ts)
- VitePress docs site: hero, getting-started, architecture, security,
  deployment, configuration, developer, supported-formats
- i18n: 10 product keys across all 21 locales (hero, app description,
  privacy notes, AI features, progress messages, getting-started)
- web/demo/landing meta + privacy copy, COMMUNITY_GUIDE, .env.example

Stale tool counts (53/50+/52/70+/35) corrected to 157 throughout.
Database/container deployment claims left unchanged (out of scope).

* docs: fix stale post-rebrand test assertions and README language list

- tests/e2e-docs/homepage.spec.ts: assert the current docs homepage (file toolkit, 157 tools, 5 modalities) instead of the old image-only strings
- tests/unit/api/docs-route.test.ts: sync the reproduced llms.txt tagline with docs.ts
- README.md: 21 languages with the correct list (add Swedish and Chinese Traditional, drop Czech which is not supported)

* docs: correct 2.0 architecture references (Postgres 17 + Redis 8, 3-container stack)

The docs and metadata still described the 1.x stack (SQLite, single container, p-queue). Update them to the current 2.0 reality.

- README: replace the broken single-container `docker run` quick-start with the real Docker Compose stack (app + Postgres 17 + Redis 8); fix the "no Redis, no Postgres" feature bullet
- package.json: description no longer claims a single container
- apps/docs: rewrite database.md for Postgres; configuration.md DB_PATH -> DATABASE_URL + REDIS_URL; architecture.md SQLite/p-queue/better-sqlite3 -> Postgres/BullMQ/pg and add media-engine + doc-engine; developer/security/deployment/docker-tags/getting-started/contributing compose examples now include postgres + redis; index.md + api/ai.md AI count 16 -> 19
- SECURITY.md: Drizzle (SQLite) -> (PostgreSQL)
- landing: enterprise/FeatureHighlights single-container wording; TrustSignals/ToolGrid 150+ -> 157 (dynamic); Pricing/FAQ 15 -> 19 AI tools

* docs(api): document all video, audio, document, and data tool endpoints in OpenAPI

The spec covered only image tools; the Scalar UI and the generated /llms.txt and /llms-full.txt inherited that gap. Add the 104 missing tool endpoints so the API docs match the code.

- Video: 29 endpoints (most long/async; auto-subtitles is AI)
- Audio: 17 (transcribe-audio is AI)
- Document/PDF: 36 (ocr-pdf is AI; conversions are long/async)
- Data: 10
- Image: 12 newer tools (background-replace, blur-background AI; histogram/lqip-placeholder/sprite-sheet custom responses; barcode-generate uses a JSON body)

Each schema is derived from the tool's Zod validator and executionHint (fast -> 200, long -> 202+SSE, AI adds 501 FeatureNotInstalledError, multi-file inputs as arrays), referencing the existing shared schemas. Tool path entries: 64 -> 168. Spec parses as valid YAML with no duplicate paths and only known $refs.
2026-06-16 18:04:52 +08:00
SnapOtterandGitHub 8eee17aeea fix: handle non-image modalities across uploads, previews, and filenames (#255)
SnapOtter spans five modalities now, but several code paths still assumed image input.

- dropzone: default to accept-all when no fileFilter is given (image tools still pass one); neutral "supported file types" error text instead of "image files"
- automate (pipelines): accept any modality in the file pickers and dropzones; render modality-aware previews (video player, audio waveform, document/data card) instead of always using ImageViewer/BeforeAfterSlider
- filename sanitizer: extend the double-extension allowlist beyond image extensions to video/audio/document/data so e.g. "report.csv.php" becomes "report.csv"; add tests
- thumbnail route: return 422 for non-rasterisable files (audio, data, non-PDF docs) instead of attempting a doomed Sharp decode
- pool: unknown tools fall back to the "system" pool, not the image pool
- a11y labels: "Previous/Next image", "Image viewer/area/controls/drop zone" are now modality-neutral, across all 21 locales
- copy: bulk-rename default, find-duplicates ZIP name, SSRF user-agent, fetch-urls fallback name, file-details MIME label, URL-import placeholder, help dialog
2026-06-16 18:04:48 +08:00
SnapOtterandGitHub d50e8e42a7 QA + image-tool depth pass: codec/eraser/PDF fixes, modality renames, 13 image tools deepened (#249)
* fix(media): mux container-correct codecs in video tools

Video tools hardcoded H.264 (and AAC) while keeping the input's container extension, so a .webm input produced an invalid file (ffmpeg exit 234: H.264 cannot be muxed into WebM). Add shared videoEncodeArgsForContainer/audioEncodeArgsForContainer helpers (vp9+opus for webm, theora+vorbis for ogv, h264+aac otherwise) and apply them across 14 tools; re-encode audio to AAC in burn-subtitles (forced mp4). Adds a webm regression test for change-fps.

* fix(eraser): recover Object Eraser when its progress SSE drops

The eraser used a bespoke EventSource with no recovery, so a dropped SSE left the UI stuck at ~25% forever even though the backend job had finished and saved its result. Add a resilient subscription (reconnect on tab refocus, which replays the cached terminal frame; 5-minute stall timeout) mirroring the standard processor's PR #203/#204 recovery.

* feat(ui): rename the Documents modality to PDF and Data to Files

Updates modality display names, the home-page tabs, the tool-page breadcrumb, and the homePage.documents/data + modalities labels across all 21 locales. URL slugs are unchanged for link stability.

* feat(compress-pdf): add quality and target-size compression modes

Mirror the image Compress tool: a quality slider (1-100) and a target file size, replacing the screen/ebook/printer preset. Adds gsCompressPdfQuality to doc-engine (quality maps to image downsample DPI, the dominant size lever for PDFs); target-size binary-searches the DPI for the highest quality under the target. The frontend reuses the shared CompressControls component, so no new translation strings are needed.

* feat(ocr-pdf): show the PDF preview and extracted text side by side

ocr-pdf fell back to the image viewer, which cannot render a PDF, so the right pane showed 'Preview not available' and the extracted text was only a download. It now uses a custom results view (custom-results display mode) rendering the input PDF via pdf.js (DocumentView gains an inputOnly prop, since the tool's output is a .txt) next to the extracted OCR text, with a copy button.

* feat(ui): link the modality breadcrumb to its tools tab

The modality segment of the tool breadcrumb (PDF, Image, Video, Audio, Files) is now a link to /?modality=<tab>. The home page reads the param, activates the matching tab, and cleans the URL, so it returns to the existing Tools page filtered to that modality without a new page. Handles the file modality whose tab key is 'data'.

* feat(circle-crop): add zoom/offset framing, border, background, and output size

Upgrade the circle-crop tool from a bare centered crop into a framing and
styling tool. New settings (all backward-compatible with the old empty
payload):

- zoom (1-5x) plus offsetX/offsetY (0-1) to control how tight the circle is
  and where it sits in the source image
- borderWidth (0-200px) plus borderColor for an optional ring
- background: transparent (clear corners) or a hex fill
- outputSize for a square output; omitted keeps native size

The settings panel gains an inline draggable circular preview that mirrors
the framing live, a zoom slider, a border slider with color, a
transparent/color background toggle, and an output-size field. Adds an
integration test covering output size, border, and a solid background.

* feat(image-tools): flesh out five thin tools (gif-webp, histogram, favicon, color-palette, lqip)

Tier A of the image-tool depth pass. Each of these was as bare as the old
circle-crop (empty settings, opaque or invisible output). Now:

- gif-webp: quality, lossless, and resize-percent controls; shows before/after size
- histogram: returns full per-channel bins + stats; the settings panel renders an
  inline interactive histogram with R/G/B/Luma toggles, linear/log scale, and a
  mean/median/stdev readout (server PNG still downloadable)
- favicon: background fill, padding, corner-radius, theme color, and a per-size
  checklist, with a live preview grid; the route applies the styling and honors
  the size filter
- color-palette: count (2-16) and hex/rgb/hsl format controls, median-cut
  extraction, a palette strip, and CSS/JSON export
- lqip-placeholder: blur/pixelate/solid strategies, format and quality; the
  output panel now surfaces the data URI with copy plus HTML/CSS snippets and a
  preview (previously the deliverable was never shown)

Also expose resultPayload from useToolProcessor so a tool can render the route's
extra result fields (histogram bins, lqip data URI) in its own panel. Updates the
five integration tests to cover the new settings.

* feat(image-tools): deepen five thin tools (duotone, vignette, pixelate, background-replace, blur-background)

Tier B of the image-tool depth pass.

- duotone: preset palettes, an intensity slider that blends the duotone with
  the original, and a true live duotone preview (a self-contained grayscale +
  lighten/darken overlay so the pane filter cannot wash it out)
- vignette: radius, softness, roundness, and center-x/y controls driving a
  rebuilt radial gradient, with a matching live overlay
- pixelate: a selection mode that exposes the route's region support via a
  draggable box over the image plus width/height sliders, so a face or plate
  can be pixelated in isolation
- background-replace: gradient backgrounds, edge feather, and webp output on top
  of the existing solid color; now shown before/after
- blur-background: edge feather and webp output; now shown before/after

The live previews for duotone and vignette needed onImageStyle to mount the
overlay branch in image-viewer. The duotone intensity blend and both AI tools'
edge feather were rewritten to splice the alpha channel through raw buffers;
joinChannel did not reliably re-tag the merged channel as alpha and a
raw-without-encoder buffer broke the next decode. Updates the five integration
tests.

* fix(data): rename Files modality to Data + 20 Data-tool bug fixes (#247)

* fix(ui): restore the Data modality name (revert Files rename)

The 'file' modality reverts to the 'Data' label in modality.ts, the home-page tab, and the tools.data + documentsAndFiles i18n keys across all 21 locales. The separate Documents to PDF rename is kept. The URL slug was already /data, so name and slug realign; the tool breadcrumb follows modality.ts automatically.

* fix(create-zip): require at least two files before enabling submit

create-zip enabled its submit button with a single file, but the backend rejects fewer than two files ('Zipping needs at least two files'), producing a 422 error. Gate the button on files.length >= 2 to match the sibling merge-csvs tool. Found during the Data-modality QA sweep.

* fix(data): resolve 17 bugs found in a deeper Data-tool review

Crashes (threw an internal error on otherwise-valid input):
- csv-json: a primitive JSON array like [1,2,3] threw "Unable to serialize"; now a clear error.
- json-xml: a null or primitive JSON root crashed the XML builder; now a clear 4xx.
- yaml-json: an empty or comment-only YAML returned undefined and threw on Buffer.from; now emits null.

Data loss / wrong output:
- csv-json: nested objects rendered as "[object Object]" (now serialized to JSON); heterogeneous objects dropped columns (now the union of all keys).
- xml-to-csv: leaked fast-xml-parser markers ("@_" on attributes, "#text") into CSV headers (now cleaned); a single-record XML failed to tabulate (now a 1-row table); heterogeneous records dropped columns (now the union of all keys).
- csv-excel: xlsx date cells were rendered in the server timezone via Date.toString (now ISO 8601, round-trippable).
- create-zip and extract-zip: filename/basename collisions overwrote zip entries and silently lost a file; dedup now checks generated names and guarantees uniqueness.
- chart-maker: negative values produced invalid/degenerate SVG that Sharp silently dropped; now rejected with a clear message.

Empty output / validation:
- split-csv: a header-only CSV produced an empty zip; now errors with "No data rows to split".
- extract-zip: a directory-only zip produced an empty zip; now errors with "No extractable files found".
- create-zip and merge-csvs: a single-file request fell through to the worker and returned 422; the factory now supports minInputs and returns 400 pre-enqueue.

UI:
- review-panel: the result card showed "Saved +X%" when the output grew; the savings row now appears only when the file is actually smaller (Original/Processed sizes always shown).

Found via two adversarial code-review passes over the 10 Data routes. All 24 fix + regression checks pass against a fresh Docker stack on :1359.

* fix(data): clean 400 for unsafe-zip entries; drop header on split keepHeader=false

- tool-factory: add an opt-in preValidate hook that runs after input prep and
  before enqueue. Throwing InputValidationError there returns its statusCode
  (400) instead of the worker's generic 422. BullMQ loses the error class across
  the job boundary, so InputValidationErrors thrown in the worker cannot be
  mapped to their status; pre-enqueue validation can.
- extract-zip: validate entry paths via preValidate, rejecting path-traversal
  and absolute-path archives (and unreadable/corrupt zips) with a clear 400. The
  processV2 guards remain as defense-in-depth for the pipeline/batch path.
- split-csv: keepHeader=false now drops the header (parts contain only data
  rows) instead of keeping it as the first data row of part-1.

Verified against a fresh Docker stack: unsafe / absolute / corrupt zips -> 400,
normal zip still 200; split keepHeader=false drops the header while true repeats
it in each part. No regressions across 51 fix + scenario checks.

* feat(image-tools): deepen image-pad and sprite-sheet, fix sprite-sheet multi-file submit

Tier C of the image-tool depth pass.

- image-pad: a custom W:H ratio alongside the presets, a background mode
  (solid color, transparent, or an Instagram-style blurred cover fill), and an
  extra padding margin. The settings panel gains a real live preview of the
  padded canvas (it previously declared live-preview but rendered nothing) via
  onImageStyle + onImageOverlay.
- sprite-sheet: PNG/WebP/JPEG output with a quality control, and the coordinate
  map it already computes is now returned and surfaced as Copy CSS (per-frame
  background-position rules) and Copy JSON exports.

Also fix a pre-existing sprite-sheet bug: with more than one image the panel
called processAllFiles, fanning out to the per-file batch route (422). It now
calls processFiles, which packs all images into a single sheet request (it is a
MULTI_FILE tool). Updates both integration tests.

* fix(media): preserve source sample rate after loudnorm (#243)

ffmpeg's loudnorm filter runs internally at 192 kHz and emits 192 kHz
unless the chain resamples back. normalize-audio and video-loudnorm
therefore produced 192 kHz output (4.3x larger files) regardless of the
input rate. Append aresample to restore the input's sample rate.
runMediaTool now exposes the input audio sample rate to its args callback.

* fix(color-palette): collapse solid-color images to one swatch

The median-cut bucket selector started bestRange at -1, so a uniform bucket
(range 0) still satisfied the > comparison and kept splitting, yielding N
identical swatches for a solid-color image. Start at 0 so only buckets with
real color spread are split.

* fix(lint): annotate implicit-any lets in saml and user-files

biome noImplicitAnyLet flagged the bare let in saml.ts (profile) and user-files.ts (stream); add derived type annotations (type-only, no behavior change). Latent on main via the turbo lint cache; surfaced when the Data changes busted the apps/api lint cache.
2026-06-16 15:16:13 +08:00
SnapOtterandGitHub d8cf979d4b fix: resolve 18 QA-discovered bugs across tools, previews, and the AI pipeline (#242)
Exhaustive QA sweep of all 157 tools. Fixes: CSP blob media, csv-excel ExcelJS interop, ocr-pdf segfault, chart-maker upload, non-PDF doc preview, RAW decode, merge-tool multi-file path, html-to-image chromium, ogv/wma/amr/ac3 preview fallbacks, meme/gif/stabilize codecs, nav+home a11y. Plus orphan-format and test-debt cleanup, the AI bundle build script, and a reusable Playwright QA harness under tests/qa/.
2026-06-15 22:26:24 +08:00
SnapOtter b76dc68682 fix: resolve Sharp 0.35.1 and BullMQ type incompatibilities after dep bumps
Sharp 0.35.1 moved FormatEnum to a namespace export and removed "avif"
from FormatEnum (now a separate literal in toFormat). BullMQ 5.78.1
bundles ioredis 5.10.1 while we have 5.11.1, causing structural type
mismatch. Also fixes new Biome 1.9 lint rules.
2026-06-15 15:20:16 +08:00
SnapOtter d3cd4e41b5 feat: on-demand preview generation with progress messages for non-native formats 2026-06-15 12:04:30 +08:00