mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
44081e6a3f60745aac00018ee13554df3771b251
1386
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c2903872bb |
chore(release): 2.2.0 [skip ci]
# [2.2.0](https://github.com/snapotter-hq/snapotter/compare/v2.1.0...v2.2.0) (2026-07-29) ### Bug Fixes * **a11y:** focus indicators meet the 3:1 non-text contrast bar ([#574](https://github.com/snapotter-hq/snapotter/issues/574)) ([ |
||
|
|
a76982da60 |
chore: reset the partial 2.2.0 release artifacts for a clean re-run
The first v2.2.0 dispatch failed at archive-security after semantic-release had already committed its release artifacts (chore(release): 2.2.0). Undo those so the re-dispatch regenerates them once instead of duplicating the CHANGELOG section: restore .release-notes.md to the root, and drop the 2.2.0 entries from CHANGELOG.md and the docs changelog. The tag and draft release were deleted, so the fresh run re-tags on this commit, which includes the #665 fixes the previous tag lacked. Non-releasable type so the re-run stays 2.2.0. |
||
|
|
4ae03b441b |
chore(release): 2.2.0 [skip ci]
# [2.2.0](https://github.com/snapotter-hq/snapotter/compare/v2.1.0...v2.2.0) (2026-07-29) ### Bug Fixes * **a11y:** focus indicators meet the 3:1 non-text contrast bar ([#574](https://github.com/snapotter-hq/snapotter/issues/574)) ([ |
||
|
|
a75a22dd3f |
fix(landing): derive conversion-preset copy from real format differences (#663)
The 83 X-to-Y converter pages were generated from one template with the format names substituted in. Three of the four features and two of the three FAQs were byte-identical across all 82 non-override presets, every page came out at exactly 144 words, and pages within a family sat at roughly 61% similarity with identical shingle counts. That is the mass-produced fingerprint, and the same shape that got the docs tool pages deduped in #662. All 83 are indexed today, so this is prevention rather than repair. Copy is now derived from what actually differs between the two formats: alpha, animation, colour depth, lossy re-encode behaviour, container reach, and where each format is genuinely accepted. png-to-jpg explains that transparency gets flattened; gif-to-jpg warns you keep frame one only; psd-to-svg says to export Photoshop's real vector layers instead of tracing them. Mean similarity across all 3,403 pairs drops from 34.8% to 6.7%, and the uniform 144-word length is gone (206-256 words, varying). Within-family max is roughly flat: on the built pages, image goes 62.8% to 66.1%, video 61.3% to 66.4%, audio 60.7% to 60.6%. Generated copy has a similarity floor, and beating it inside a family would take hand-written pages. What has gone is the identical shingle count per family, which was the strongest tell. Also drops the one-off eps-to-svg override: all five vectorize presets now get honest tracing copy, not just that one. Verified: landing builds 798 pages, zero verbatim repeats between a page's blurb and its own features or FAQs, biome clean. |
||
|
|
42e1dc9799 |
fix(docs): keep the translated locale trees out of the search index (#662)
Search Console flagged four reasons on 2026-07-28: soft 404, both duplicate-canonical variants, and noindex. All four sat on docs.snapotter.com; the landing site was clean. The cause was boilerplate dominance rather than a broken tag. /tools/video/crop-video carries 1.3 KB of unique body against 3.5 KB of identical chrome (nav, sidebar, 21-language switcher), so unrelated tool docs measured 52-60% full-page similarity. Across 20 locales that was 3,640 of 3,822 submitted URLs. Google read the lot as one duplicate cluster and began electing arbitrary representatives: /changelog became the canonical for /tools/image/favicon, and /uk/guide/getting-started for /nl/tools/image/resize. English tool docs indexed 2 of 10 and localized 4 of 10, while the landing page for those same five tools indexed 5 of 5. Translated pages now emit `noindex, follow` with a self-canonical, and sitemap.transformItems drops them from the sitemap. Self-canonical rather than pointing at English, since noindex paired with a cross-canonical sends two conflicting instructions. hreflang is removed outright: the annotation only means something between pages that can all be indexed. Readers see no change. The language switcher and every in-page link behave exactly as before. Verified against a real build: sitemap 3,822 to 182 URLs with zero translated entries, all 3,640 translated files carrying the noindex and no English file doing so, docs e2e 100 passed. |
||
|
|
5f21588f6c |
chore: prepare the 2.2.0 release (#660)
Bumps every version surface to 2.2.0, fixes a latent version-coupling bug in the OCR runtime tests, and stops an absent GPU runner from silently stalling a release. Version surfaces: scripts/sync-version.sh covers the 11 workspaces, APP_VERSION, and the docs release commands across all locales. Root package.json plus the three surfaces the script never reaches are done by hand: the DOCKERHUB.md banner and tag table, the docker-tags.md pinning table in 21 locales, and the example runtimeVersion in tools/image/ocr.md in 21 locales. The release-notes archive step is deliberately not pre-run, so the notes text stays editable until the release. Latent bug: runtime-state rejects any runtime whose compatibility.snapotterVersion is not exactly APP_VERSION, and five fixtures pinned the literal 2.1.0. Since semantic-release rewrites APP_VERSION on every release, the first PR after any bump would have gone red for a reason nobody would trace to the release. The fixtures now derive from APP_VERSION. GPU runner: sign-ocr-index needs verify-ocr-nvidia on self-hosted hardware, and the gated manifest job needs ai-bundles, so a missing runner queued instead of failing and produced no image tags. preflight-gpu-runner claims the same labels with no dependencies, so it is scheduled first and validates the GPU before the 90-minute build. An API preflight is impossible because listing self-hosted runners needs Administration:read, which GITHUB_TOKEN cannot hold, so RELEASE.md carries the maintainer-side check. |
||
|
|
d10d0f544f |
fix: release QA hardening across processing, media, security, and CI gates (#649)
A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch. |
||
|
|
2848dd0e53 |
fix(help): render the help dialog from i18n instead of hardcoded English (#647)
The help dialog carried its 13 shortcut labels, its getting-started paragraph and its version line as hardcoded English, while fully translated strings for exactly those labels sat unused in all 21 locale files. Every non-English user read English there. The translations did not need writing, only reading: t.help.keyboardShortcuts already had focusSearchBar, goToTools, processFile and the rest, in every locale. Labels now index into t.help.keyboardShortcuts by key rather than carrying text. Getting-started reads t.help.gettingStarted.description, which drops the inline Kbd chip the hardcoded copy had, matching what all 21 locales already say. The version line goes through t.help.versionLabel. Also adds the type-to-search row that #644 left out, keyed help.keyboardShortcuts.typeToSearch, translated into all 21 locales, and regenerates the two darwin help-dialog baselines for the extra row. Nothing caught the original bug because the i18n context defaults to en, so asserting on English text passes whether or not the component reads i18n at all. The new test mocks the context with sentinel values instead: putting the hardcoded labels back fails 15 of its 19 cases. Verified: 19 new unit tests, full unit suite 7576 passed, help-dialog visual 3/3 against regenerated baselines, help accessibility e2e 7/7, typecheck and lint clean, all 18 CI checks green. |
||
|
|
2d8b57c57f |
fix(api): gate every tool endpoint and stop ZIP streams failing quietly (#646)
Three defects from #645, all of which let the server report something that was not true. Tool access was enforced per route, so it drifted. createToolRoute calls requireToolAccess and the factory tools were fine, but all 45 hand-written routes had to remember the same call and none of them did. A role without tools:use could run image-to-pdf, svg-to-raster, erase-object, favicon, qr-generate, upscale, sign-pdf and the rest. The issue described this as affecting two routes; it was every one of them. The check now lives in a single preHandler keyed off the tool the router matched, so it covers sub-paths (/batch, /info, /preview, /analyze, /inspect) and any route added later without that route opting in. Ids no tool claims stay unresolved, which keeps an unknown or misfiled tool a 404 rather than telling an unauthorized caller which ids exist. Resolution reads request.routeOptions.url, the pattern the app itself registered, rather than parsing request.url a second time. find-my-way decodes before matching, so an independent parse disagrees with the router and the router wins: `/api/v1/tools/image/%66avicon` ran favicon while the gate saw no tool at all. Absolute-form request targets slipped it the same way. Taking the router's own answer removes the disagreement. A ZIP stream that failed after the 200 headers were out called reply.raw.end(). On a chunked response that is indistinguishable from success, so a client kept an archive with no central directory believing it whole. Worse, a source stream that errored had no listener: the request hung until it timed out and the error surfaced as unhandled. A poisoned-storage probe reproduced both. The socket is destroyed instead, and every source stream is listened to. svg-to-raster additionally ran its append loop past the hijack with no try/catch, where a throw leaves Fastify logging and walking away with the socket neither ended nor destroyed. pdf-to-image is fixed alongside the other two: it shipped in #643 with the destroy half but not the listener, so it hung the same way. A zero-byte upload was dropped during parsing. The client pairs results with its own file list by index, so every later result shifted onto the wrong file: one document's output was presented as another's, under another's name, while the file that actually converted was marked "not found in batch results". Empty parts now keep their slot and fail in place with a reason. Two tests in adversarial-extended.test.ts asserted the old zero-byte behavior, including a comment that batch "silently skips zero-byte parts". They now pin the replacement: still rejected, nothing processed, but the caller is told which files were empty instead of being told it sent none. A guard walks the whole catalog and fails if any of the 241 tools answers anything but 403 for a role without tools:use, so a tool cannot escape the gate by being registered in a shape nobody thought to sample. Fixes #645 |
||
|
|
0058fc610f |
feat(search): start typing anywhere to fill the search box (#644)
Type a printable character on the landing homepage or the app's home
dashboard and it lands in the search box, provided the box is on screen
and nothing else holds focus. Mod+K keeps working unchanged.
The parts that are easy to get wrong live in
packages/shared/src/search/type-to-search.ts so the two surfaces cannot
drift. isTypeToSearchKey decides whether a keystroke is text.
isSearchBoxTypeable decides whether the box is reachable, via one
elementFromPoint hit test at its center, which folds off-screen,
covered-by-a-modal and hidden into a single check that leans on no one's
aria markup. It fails closed where there is no layout engine, so jsdom
tests that mount the search bar do not blow up on it.
Modifier handling reads getModifierState("AltGraph") rather than
inferring AltGr from ctrl+alt. That inference reads correctly on Windows
and is backwards on macOS, where Option alone types accented characters
and ctrl+alt is a shortcut prefix, VoiceOver's included.
Focus is claimed before the keystroke is committed. Browsers silently
refuse focus inside inert or visibility:hidden subtrees, and without the
check an entire query drains into a box the user cannot see.
Scope comes from where the hook is mounted rather than a route check that
could rot, so tool pages, the editor, Files and Automate get nothing. No
new i18n strings, and no new analytics event, since
ANALYTICS_EVENTS.SEARCH already fires off the same state change.
Verified: 44 new unit tests, full unit suite 7557 passed, landing
homepage 24/24, home-page 19/19, gui-keyboard 41/41, typecheck and lint
clean, all 18 CI checks green.
|
||
|
|
a7137958a1 |
fix(pdf): pdf-to-image presets no longer 404 on 2+ files (#643)
Upload two PDFs to pdf-to-jpg and it answered `Tool "pdf-to-jpg" not found`. pdf-to-jpg, pdf-to-png and pdf-to-tiff share registerPdfToImageRoute, which registered a single-file endpoint and nothing else, so the shared preset settings component's 2+-file submission fell through to the generic `:section/:toolId/batch` route, whose registry lookup misses every tool outside createToolRoute/registerToolProcessFn. Mirror of #627, different fix. image-to-pdf is many-to-one, so #633 sent every file in one request. This direction is one-to-many: separate PDFs want separate conversions, which is what /batch is for. The route now serves its own /batch, the shape svg-to-raster already uses, and the literal path beats the generic parametric one. One PDF fans out to many page images, so a per-file result is a ZIP, same as the single-file route. A batch returns a ZIP of per-document ZIPs in upload order, keyed by X-File-Results so each result pairs with the file it came from. A document that is unreadable, locked, empty, short of the requested page range, or carrying no pages at all fails alone; 422 with a reason per file when none survive. That literal path also shadows the generic route's requireToolAccess call, which would have turned a 403 into a converted ZIP for roles without tools:use. All four endpoints in this file now gate. Four ways the batch path could have reported something untrue are closed with it: a storage fault blamed on the document (statusCode-carrying errors now reach the error handler, the rest are logged before being reduced to a generic message), per-file reasons stranded in a field parseApiError never reads, a zero-byte upload dropped so that later results landed on the wrong file, and a mid-stream failure ended cleanly enough to pass for success (the socket is destroyed instead). Page rendering and ZIP assembly are shared helpers now, createUniqueNamer moves to lib/filename.ts next to its two existing copies, and tool-route-drift fails if any batch-dispatched preset loses its /batch route. Follow-up for the same defects in the sibling custom routes: #645. Fixes #632 |
||
|
|
d690a6e26d |
feat(landing): add a live system status indicator to the footer (#641)
Adds /api/status to the landing Pages worker, HEAD-probing demo.snapotter.com and docs.snapotter.com with a 2 second per-attempt deadline and one retry. snapotter.com is not probed; the worker answering the request is the proof it is up. The footer badge ships grey in the static HTML and only upgrades once the route answers. A rejected fetch, a non-ok response, an unparseable body, and an unrecognized verdict all leave it grey, so it never claims green on its own. Color lives in the dot, never the label: `--color-success` scores 4.498:1 against the footer's `--color-background-alt`, just under AA. Four labels across 21 locales. |
||
|
|
5cc0a850c6 |
feat: add landing testimonial wall, unblock the onboarding survey (#639)
Testimonial wall: new landing section between Feature Highlights and Pricing. Two rows drifting in opposite directions, CSS-only to keep the zero-runtime rule. 22 quotes, all verbatim and traceable to a public URL or a feedback_submitted event. In-app quotes ship unattributed because the feedback dialog only ever promised "You can contact me about this feedback". Marquee traps documented in the CSS: a track gap also sits between the last original and the first clone, so the -50% translate jumped half a gap per loop; and under dir="rtl" the flex track drifted itself off-screen while "@amn-96" bidi-reordered to "amn-96@". Landing stats: DOCKER_FALLBACK read 104,000 against a real 233,057, but the stale constant was the symptom. Both fetchers swallowed failures in a bare catch, so a degraded build never announced itself. That warning then exposed the real bug: getStarCount runs from Navbar and TrustSignals on all 798 pages, firing ~800 unauthenticated GitHub calls per build and 403ing partway through, so early pages carried the live count and later pages the fallback. Both fetchers now memoize the promise. Onboarding survey: the shipped gate has no activity condition, so it fires on first admin login; 1,105 of 1,287 surveyed instances never processed a file. The opaque fixed inset-0 aria-modal with a focus trap and no Escape becomes a corner card at 12% of the screen, Escape closes, and the optional questions stay collapsed until the one required answer. Its title was an h1, which RouteAnnouncer focuses and announces on every route change, so navigating anywhere announced the survey instead of the page. Now an h2. |
||
|
|
025851beef |
fix: honor unlimited processing timeout (#638)
* fix(web): recover stalled job progress streams * fix(ai): honor unlimited processing timeout * fix(web): keep retrying stalled progress streams |
||
|
|
841f47f6ca |
fix(files): decode CLI-decoded formats before URL-import preview generation (#637)
fetch-urls.ts called sharp(buffer).webp() directly on the raw, undecoded buffer for CLI-decoded formats (HEIC, RAW, PSD, ...), which throws and gets silently swallowed, so URL-imported files in those formats came back with no preview and 0x0 dimensions. Decodes once before both preview generation and dimension lookup, mirroring the pattern already used elsewhere in the codebase. Fixes #634 |
||
|
|
511633fa1c |
fix(files): store null instead of 0x0 for undecoded upload dimensions (#636)
validateImageBuffer() intentionally reports {width: 0, height: 0} for every CLI_DECODED_FORMATS member. The file library's upload and save-result endpoints treated that 0 as a real measurement and wrote it into the DB. Adds a measuredDimensions() helper that treats non-positive width/height as unmeasured and stores null instead.
Fixes #635
|
||
|
|
098ed50d06 |
fix(image): decode real iPhone HEIC files instead of rejecting them at validation (#631)
validateImageBuffer() never listed heif in CLI_DECODED_FORMATS, so real iPhone HEIC uploads hit Sharp's own metadata probe (its bundled libheif only supports AV1/AVIF) and got rejected before reaching the working heif-convert/heif-dec decode path already wired up downstream. Adds heif to that set, same as raw/psd/tga/bmp/etc. Also fixes the same gap on erase-object's mask input, which validates through the same function but had no matching decode step, so a HEIC mask reached an unguarded sharp() call and came back as a misclassified server error instead of a clean 422. Fixes #622 |
||
|
|
330cf559e0 |
fix(image): image-to-pdf presets no longer 404 on 2+ files (#633)
jpg-to-pdf and its six image-to-pdf-group siblings share the base tool's registerImageToPdfRoute, which never registers into the toolRegistry the generic /batch endpoint reads from. The shared conversion-preset settings component routed any 2+-file submission to /batch regardless of tool, so these presets 404'd with `Tool "<id>" not found` past the first file, while the base image-to-pdf tool stayed unaffected because it bypasses that dispatch entirely with its own settings component. MULTI_FILE_TOOLS now includes every image-to-pdf-group preset, derived from BASE_CONFIG instead of hardcoded, and the preset settings component checks that set before choosing batch vs. a single combined request. Fixes #627 |
||
|
|
e0a7aecde8 |
fix(pdf): restore downloads on PDF conversion preset pages (#629)
Adds downloadUrl/originalSize/processedSize to the pdf-to-image route's synchronous response so PDF conversion presets (pdf-to-png, pdf-to-jpg, pdf-to-tiff) satisfy the standard tool-result contract and show their download action again. Fixes #623 Co-authored-by: EuanTop <euan@mail.bnu.edu.cn> |
||
|
|
301e6eb01a |
test: coverage campaign and mutation testing across five packages (#628)
Coverage 83.6 to 87.36% lines, 81.63 to 84.14% branches. Mutation testing across five packages: image-engine 85, media-engine 92, doc-engine 87, shared+enterprise 86, apps/api security and jobs slice. Runs all five lanes weekly. Fixes the silently-broken mutation CI (babel pin), a redact-pdf envelope-shape test bug, an untested enterprise license valid-signature path, and an audit test that only exercised a hand-copied reproduction. Test and config only, no product code changes beyond the babel pin and one test-only oidc export. Full suite: 16,712 pass, 0 fail. |
||
|
|
44f5aea326 |
fix(ci): repair the chronically-failing nightly workflow (#624)
The scheduled Nightly had been red for over a week across nearly every job. This root-causes and fixes each one. All were pre-existing: missing CI provisioning, specs that drifted as the app grew, a job too heavy for its timeout, and a fuzz that was never configured for file-upload endpoints. None came from the recent security merge. - Coverage + Docker Container E2E: install tesseract and its language packs so the built-in Fast OCR tests stop throwing spawn ENOENT; gate two repo-file and release-workflow tests that cannot run inside the slimmed container image. - E2E (Full, Serial, Cross-Browser, Device Matrix): refresh specs that drifted behind the app (tool renames, the now admin-only Tools tab, dropped About copy, locator collisions scoped to the right region). One real product fix rode along: /config/auth was refetched six times per tool-page load, so cache it behind a single shared fetch, dropping the tool page from 13 to 8 API calls. - Extended Matrix + Fuzz: shard the integration suite four ways so the full format x tool matrix plus property fuzz fits its budget instead of overrunning the 90-minute ceiling every night. - Schemathesis: exclude the tools with bespoke handlers that process synchronously in-request (they hang the fuzz on adversarial input) and suppress Hypothesis's data-generation health checks, which fire because file-upload endpoints reject the fuzzer's random bytes. not_a_server_error still runs on every generated case (5000+ per run). - Stabilize two long-tail flakes: raise the avif matrix per-test cap from 240s to 600s, and assert toHaveCount(0) on the deleted user row so a transient success toast no longer trips a strict-mode violation. Verified end to end: the full Nightly workflow is green on this branch (all 14 jobs), and PR CI is green. |
||
|
|
079fcd2631 |
fix(security): close the gaps a full 2.0 re-audit left open (#620)
Follow-up to a full re-audit of the 2.0 tree. Most prior findings were already fixed; this closes the ones that were not: - SAML assertion replay: validateInResponseTo ifPresent plus a Redis-backed CacheProvider, so a captured signed assertion cannot be replayed. ifPresent keeps IdP-initiated SSO working. - MFA login challenge burned after 5 wrong TOTP codes. - api_keys.key_prefix indexed; the per-request lookup was a full table scan. - MAX_AI_JOBS_PER_USER caps a user's in-flight single-file AI jobs (the AI pool runs at concurrency 1). Batch and pipeline AI stay uncapped. - MAX_WORKSPACE_SIZE_GB enforced instead of being dead config. - SUBPROCESS_MEMORY_LIMIT_MB (default off) for the native media and doc engines; not applied to the AI sidecar. - SVG sanitizer closes unquoted and whitespace-prefixed javascript: hrefs and the animateTransform/animateMotion/handler/mpath elements. - Windows-style paths stripped from error output to match the Sentry scrubber. - Postgres and Redis compose services get cap_drop plus pids_limit and cpus. - .env.example ships MAX_SVG_SIZE_MB=50 (0 disabled the cap). Adds security-focused unit and integration tests. typecheck, biome, and the full unit and integration suites pass. |
||
|
|
44d8109486 |
fix: enforce settings authority boundaries (#618)
Close generic settings authorization bypasses and enforce per-setting authority, validation, redaction, transactional config import, and route-local write rate limiting. |
||
|
|
0467e87bfe |
fix(download): reset the socket when a stream is shorter than Content-Length (#617)
The download route sets Content-Length from a stat and then streams the object; when the stat size exceeds the bytes the stream yields (#590 "cause 2"), the client hangs on keep-alive framing waiting for a tail that never arrives. Both send paths now run through a backpressure-safe byte-counting Transform that resets the socket on a shortfall, so the download fails at once instead of hanging. Adds a real-socket regression test at the generic download route, the coverage gap #590 named. Refs #590 Co-authored-by: harshjainnn <170849281+harshjainnn@users.noreply.github.com> |
||
|
|
1f8a42e548 |
fix: enforce role authority for user management (#616)
Centralize role-authority enforcement across user management, role management, configuration import, SCIM, GDPR, and MFA mutations. Add regression coverage for delegated custom roles and protect higher-privilege accounts from reset, deletion, or takeover. |
||
|
|
129e42b95c |
feat(feedback): gate onboarding survey on first processing, add prompt lifecycle events (#615)
Defers the onboarding usage survey to the instance's first successful processing (the worker writes a one-time onboarding.firstProcessedAt marker and the overlay gates on it), so it reaches engaged users instead of first-landing visitors. Replaces the two questions telemetry already answers (modality preference from tool_used, install method from instance_started) with what it can't infer: prior tool, self-host motivation, and discovery source. Adds feedback_prompt_shown and feedback_prompt_dismissed on all five feedback surfaces (usage survey, per-job prompt, admin install card, global nav dialog, search-miss) so skip and completion rates are measurable, not just submissions. New survey strings translated into all 20 non-English locales. |
||
|
|
b20bca3c3c |
fix(telemetry): data-quality pass (opt-in noise, onboarding split, file_count, OIDC) (#614)
Five fixes to the PostHog event stream, from an audit of what we actually collect versus what's flowing in. Each one is test-first. ## What changed **Silenced the `$opt_in` noise.** `initAnalytics` called `opt_in_capturing()` on every page load to clear a stale opt-out flag, and posthog-js emits an `$opt_in` event on every call. That was 10k+ events a month (up to 55 per user) carrying no signal: analytics is on by default with an admin opt-out, so there is no per-user consent to record. Both call sites now pass `captureEventName: false`. **Split the onboarding survey out of `feedback_submitted`.** The onboarding usage survey rode the same event as real feedback, so about 93% of "feedback" was actually onboarding profiling. It now emits `onboarding_survey_submitted`, so feedback metrics mean feedback again. **Set `pipeline_executed.file_count`.** It was declared in the properties interface but never populated. A pure `pipelineExecutedProps` helper now derives it (batch size for a batch run, else 1) and is shared by the success and failure paths, which also drops a duplicated payload. **Tracked OIDC login failures.** All six OIDC callback failure branches bumped the Prometheus counter and wrote an audit log but never emitted `auth_login_failed`. A `recordOidcFailure` helper mirrors the password path. **Added `TELEMETRY.md`.** A contributor-facing event dictionary: every event, its properties, where it fires, and the privacy invariants, with the allowlists as source of truth. A drift test fails if any `ANALYTICS_EVENTS` value goes undocumented. I left the published telemetry guide (`apps/docs/guide/telemetry.md`) alone. It is high-level and still accurate, and editing it would pull in the 21-locale stale-gate for no gain. ## Verification - Unit (63 tests): `analytics-events`, `telemetry-doc-drift`, `api/analytics`, `web/analytics`, `worker.behavior` - Integration (41 tests): `oidc-auth`, `feedback` - Full typecheck across all 9 workspaces - Biome clean on the changed files All green locally. |
||
|
|
6a0768b39d |
fix(telemetry): surface AI sidecar and DOMException failure reasons in Sentry (#612)
AI sidecar failures reached Sentry as 'Error: Error': the scrubber type-onlys plain Errors and the tool wrappers threw them from result.error. The bridge now exports toSidecarError(), wrapping the sidecar reason in a SafeError (memory-allocation text classifies as operational, the rest as bug); all 14 wrappers use it, plus the dispatcher crash/stdin/spawn rejection paths and parseStdoutJson. toBgRemovalError from #535 delegates to the shared helper. On the web side, DOMExceptions report their specific name via err.name, so the NATIVE_ERRORS allowlist dropped the whole family's browser-authored messages. It now carries the full WebIDL DOMException name table; messages still pass through url/path redaction. Bridge-mocking test files switched to importOriginal passthrough mocks. |
||
|
|
82f5708193 |
fix(telemetry): fingerprint stackless uncaught errors so they stop collapsing (#611)
Stackless uncaught errors reached Sentry as a bare Error with no frames and collapsed into one ungroupable issue. beforeSend now fingerprints frameless events by safe identity (name, code, one-way hash of the message) so distinct crashes separate without leaking PII. Only frameless events are touched; an upstream fingerprint is never overridden. |
||
|
|
e537cb0401 |
fix(jobs): classify BullMQ stall (UnrecoverableError) as operational (#610)
BullMQ raises UnrecoverableError when a job loses its lock (a stall), e.g. a heavy upscale under memory pressure. We never throw it ourselves, so classifyError now treats it as operational (one warning per hour) instead of a bug. ReplyError stays a bug. |
||
|
|
e3c93333ea |
docs(deploy): note that proxy response buffering stalls downloads (#607)
Document that a response-buffering reverse proxy is the usual cause of a self-hosted download that starts but never finishes, point at the X-Accel-Buffering: no safety net (#604), and call out downloads alongside SSE in the nginx and Caddy examples. Refs #590 |
||
|
|
c706a9e8a1 |
docs(deploy): pin pg_isready to POSTGRES_DB and flag the default password (#606)
Add -d snapotter to the guide's Compose healthcheck examples so they match the shipped compose fix (#595), and note "change this" next to the default POSTGRES_PASSWORD. English guide only; locale docs regenerate through the i18n pipeline. Refs #592 |
||
|
|
d43208b85e |
fix(ai): warn that upscale and background removal are slow without a GPU (#605)
Add a one-line notice to the upscale and background-removal settings that heavy AI runs much slower without a GPU and a large image can take minutes, so a CPU-only self-hoster isn't caught off guard by a slow run. Translated into all 21 locales. Refs #591 |
||
|
|
89d75853f4 |
fix(download): ask reverse proxies not to buffer file downloads (#604)
Send X-Accel-Buffering: no on file download responses so nginx and compatible reverse proxies stream them through instead of buffering, the usual cause of a self-hosted download that "starts but never finishes". The app already delivers exactly Content-Length bytes; a new real-socket test proves it for both the collated PDF and the multi-file ZIP. Refs #590 |
||
|
|
e7ffb37e98 |
feat(image): add rounded-square and squircle crop tool (#602)
Adds a Rounded Crop image tool for logo, favicon, and app-icon work. It masks the framed square to a rounded rectangle (with a corner-radius control) or an iOS-style squircle, reusing circle-crop's zoom/offset framing, border ring, background fill, and output-size options. Includes translations across all 21 locales. Closes #601 |
||
|
|
7d37f6e6f5 |
fix(pdf): flag scanned PDFs in pdf-to-text and serve text as UTF-8 (#603)
When a PDF has no text layer (scanned or image-only), pdf-to-text now returns a 422 that points at the OCR tool instead of a silent empty file, and text downloads carry charset=utf-8 so UTF-8 Arabic renders correctly when the .txt is viewed inline. Fixes #589 |
||
|
|
43334324c4 |
fix(api): contain library stored-name path traversal (#600)
The library file-storage helpers joined FILES_STORAGE_PATH with a database stored_name and never checked containment, so a crafted name could read or delete files outside the storage root after a malicious 1.x SQLite import (which copies stored_name verbatim). Add assertSafeStoredName() and apply it in every helper that resolves a stored name to a path, matching the containment guard object-storage already uses. Reported by Alpesh Bhagwatkar. |
||
|
|
37c915c3da |
fix(jobs): make timeout messages tool-agnostic and CPU-aware (#596)
Replace the job timeout message that hardcoded "background-removal" for every tool with a tool-agnostic one that sets the CPU-vs-GPU expectation, the usual reason heavy AI times out on modest hardware. The client-side SSE stall message gets the same treatment. Both stay under friendlyError's 280-char limit so the guidance reaches the user instead of collapsing to a generic "Processing failed". Refs #591 |
||
|
|
e6718ada71 |
fix(seo): emit clean docs URLs and de-index the demo (#598)
The docs sitemap listed .html URLs that Cloudflare Pages 308-redirects to their clean form, while every page's canonical/hreflang already pointed at the clean URL. Google indexed the redirecting .html variant and picked its own canonical (GSC "Duplicate, Google chose different canonical"), and burned crawl budget re-fetching redirecting URLs. Setting VitePress cleanUrls:true makes the sitemap and internal links extension-less so they match the canonicals. CF Pages already serves the clean URL at 200 and 308s the .html form, so no hosting change is needed and legacy .html hits consolidate. Also mark demo.snapotter.com noindex: it mirrors the real app under the same domain property, so its routes were being crawled and indexed as thin, duplicate pages. |
||
|
|
577d74bdb1 |
fix(api): enforce job ownership on cancel endpoint (#599)
The job cancel endpoint authenticated the caller but never checked that the job belonged to them, so any authenticated user could cancel another user's job by ID. Load the job's owner and allow cancellation only for the owner or a caller with files:all; return 404 for missing and non-owned jobs alike. Extract the route into a shared registerJobRoutes() so the ownership check is covered by tests. Reported by Alpesh Bhagwatkar. |
||
|
|
a6bce6825a |
fix(editor): repair rotate/flip/resize, levels/curves, filters, and layer lock (#597)
Fixes rotate/flip/image-size raster ops, wires Levels/Curves into the render pipeline, corrects Posterize/Threshold, seeds visible Filter-menu defaults, enforces layer lock, refreshes the histogram on edits, stops revoking in-history blob URLs, and removes the dead selection Feather control. Verified: editor e2e 168/168, web typecheck + biome clean, 226 editor unit tests pass. |
||
|
|
73df107758 |
fix(pdf): stop page tools failing on short and encrypted PDFs (#594)
Empty the hardcoded page-range default in remove/split/extract PDF tools (remove-pages defaulted to "2,4-6", out of range for any PDF under 6 pages) and disable submit until a range is entered. Reject password-protected PDFs up front for PDF-only tools with guidance to unlock first, instead of failing cryptically in the qpdf worker. Adds integration + e2e coverage. |
||
|
|
df92f7ee42 |
fix(video): write faststart mp4/mov output from stabilize-video (#593)
Add -movflags +faststart for mp4/mov/m4v output from stabilize-video so the stabilized result streams and previews progressively instead of appearing broken or corrupted (moov atom was landing after mdat). Fixes #588 |
||
|
|
a361f94584 |
chore(deps-dev): bump the dev-deps group across 1 directory with 6 updates (#586)
Bumps the dev-deps group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.3` | `2.5.4` | | [semantic-release](https://github.com/semantic-release/semantic-release) | `25.0.7` | `25.0.8` | | [turbo](https://github.com/vercel/turborepo) | `2.10.4` | `2.10.5` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.2` | `4.3.3` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.2` | `4.3.3` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.3` | `8.1.5` | Updates `@biomejs/biome` from 2.5.3 to 2.5.4 - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.4/packages/@biomejs/biome) Updates `semantic-release` from 25.0.7 to 25.0.8 - [Release notes](https://github.com/semantic-release/semantic-release/releases) - [Commits](https://github.com/semantic-release/semantic-release/compare/v25.0.7...v25.0.8) Updates `turbo` from 2.10.4 to 2.10.5 - [Release notes](https://github.com/vercel/turborepo/releases) - [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md) - [Commits](https://github.com/vercel/turborepo/compare/v2.10.4...v2.10.5) Updates `@tailwindcss/vite` from 4.3.2 to 4.3.3 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-vite) Updates `tailwindcss` from 4.3.2 to 4.3.3 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss) Updates `vite` from 8.1.3 to 8.1.5 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.5.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: semantic-release dependency-version: 25.0.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: tailwindcss dependency-version: 4.3.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: turbo dependency-version: 2.10.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: vite dependency-version: 8.1.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
e7d3862c34 |
docs: GPU-runs-on-CPU troubleshooting note (#587)
Surface how to verify NVIDIA CUDA acceleration and recover when AI tools fall back to CPU despite --gpus all. Adds a Verify GPU acceleration section to the deployment guide (check logs, reinstall the affected bundle to restore the GPU ONNX Runtime build) and a pointer from the getting-started NVIDIA tip. Addresses #490. |
||
|
|
16d8891fd5 |
chore(deps): bump the production-deps group with 15 updates (#585)
Bumps the production-deps group with 15 updates: | Package | From | To | | --- | --- | --- | | [@fastify/cookie](https://github.com/fastify/fastify-cookie) | `11.1.1` | `11.1.2` | | [@scalar/fastify-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/fastify) | `1.62.5` | `1.62.9` | | [@sentry/node](https://github.com/getsentry/sentry-javascript) | `10.65.0` | `10.66.0` | | [bullmq](https://github.com/taskforcesh/bullmq) | `5.80.2` | `5.80.9` | | [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | `5.10.0` | `5.10.1` | | [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.41.0` | `5.45.2` | | [zxing-wasm](https://github.com/Sec-ant/zxing-wasm) | `3.1.1` | `3.1.2` | | [lucide](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide) | `1.24.0` | `1.25.0` | | [@sentry/react](https://github.com/getsentry/sentry-javascript) | `10.65.0` | `10.66.0` | | [fuse.js](https://github.com/krisk/Fuse) | `7.4.2` | `7.5.0` | | [posthog-js](https://github.com/PostHog/posthog-js) | `1.399.2` | `1.404.1` | | [react-colorful](https://github.com/omgovich/react-colorful) | `5.7.0` | `5.8.0` | | [wavesurfer.js](https://github.com/katspaugh/wavesurfer.js) | `7.12.10` | `7.12.11` | | [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1085.0` | `3.1090.0` | | [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.1085.0` | `3.1090.0` | Updates `@fastify/cookie` from 11.1.1 to 11.1.2 - [Release notes](https://github.com/fastify/fastify-cookie/releases) - [Commits](https://github.com/fastify/fastify-cookie/compare/v11.1.1...v11.1.2) Updates `@scalar/fastify-api-reference` from 1.62.5 to 1.62.9 - [Release notes](https://github.com/scalar/scalar/releases) - [Changelog](https://github.com/scalar/scalar/blob/main/integrations/fastify/CHANGELOG.md) - [Commits](https://github.com/scalar/scalar/commits/HEAD/integrations/fastify) Updates `@sentry/node` from 10.65.0 to 10.66.0 - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/10.65.0...10.66.0) Updates `bullmq` from 5.80.2 to 5.80.9 - [Release notes](https://github.com/taskforcesh/bullmq/releases) - [Commits](https://github.com/taskforcesh/bullmq/compare/v5.80.2...v5.80.9) Updates `fast-xml-parser` from 5.10.0 to 5.10.1 - [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases) - [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.0...v5.10.1) Updates `posthog-node` from 5.41.0 to 5.45.2 - [Release notes](https://github.com/PostHog/posthog-js/releases) - [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.45.2/packages/node) Updates `zxing-wasm` from 3.1.1 to 3.1.2 - [Release notes](https://github.com/Sec-ant/zxing-wasm/releases) - [Changelog](https://github.com/Sec-ant/zxing-wasm/blob/main/CHANGELOG.md) - [Commits](https://github.com/Sec-ant/zxing-wasm/compare/v3.1.1...v3.1.2) Updates `lucide` from 1.24.0 to 1.25.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.25.0/packages/lucide) Updates `@sentry/react` from 10.65.0 to 10.66.0 - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/10.65.0...10.66.0) Updates `fuse.js` from 7.4.2 to 7.5.0 - [Release notes](https://github.com/krisk/Fuse/releases) - [Changelog](https://github.com/krisk/Fuse/blob/main/CHANGELOG.md) - [Commits](https://github.com/krisk/Fuse/compare/v7.4.2...v7.5.0) Updates `posthog-js` from 1.399.2 to 1.404.1 - [Release notes](https://github.com/PostHog/posthog-js/releases) - [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-js/compare/posthog-js@1.399.2...posthog-js@1.404.1) Updates `react-colorful` from 5.7.0 to 5.8.0 - [Release notes](https://github.com/omgovich/react-colorful/releases) - [Changelog](https://github.com/omgovich/react-colorful/blob/master/CHANGELOG.md) - [Commits](https://github.com/omgovich/react-colorful/compare/5.7.0...v5.8.0) Updates `wavesurfer.js` from 7.12.10 to 7.12.11 - [Release notes](https://github.com/katspaugh/wavesurfer.js/releases) - [Commits](https://github.com/katspaugh/wavesurfer.js/compare/7.12.10...7.12.11) Updates `@aws-sdk/client-s3` from 3.1085.0 to 3.1090.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1090.0/clients/client-s3) Updates `@aws-sdk/lib-storage` from 3.1085.0 to 3.1090.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1090.0/lib/lib-storage) --- updated-dependencies: - dependency-name: "@fastify/cookie" dependency-version: 11.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-deps - dependency-name: "@scalar/fastify-api-reference" dependency-version: 1.62.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-deps - dependency-name: "@sentry/node" dependency-version: 10.66.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps - dependency-name: bullmq dependency-version: 5.80.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-deps - dependency-name: fast-xml-parser dependency-version: 5.10.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-deps - dependency-name: posthog-node dependency-version: 5.45.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps - dependency-name: zxing-wasm dependency-version: 3.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-deps - dependency-name: lucide dependency-version: 1.25.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps - dependency-name: "@sentry/react" dependency-version: 10.66.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps - dependency-name: fuse.js dependency-version: 7.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps - dependency-name: posthog-js dependency-version: 1.404.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps - dependency-name: react-colorful dependency-version: 5.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps - dependency-name: wavesurfer.js dependency-version: 7.12.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-deps - dependency-name: "@aws-sdk/client-s3" dependency-version: 3.1090.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps - dependency-name: "@aws-sdk/lib-storage" dependency-version: 3.1090.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
bda4db3f35 |
fix(ocr): unblock and harden accurate-OCR install (#552)
Two OCR-install fixes surfaced while verifying the accurate-OCR (v3 runtime) path end to end: - Installer timeout must be a safe integer, not a performance.now() float. With the default INSTALL_MAX_MS this failed every accurate-OCR install via the app right after the download (masked by the unpublished runtime; CI drives install_runtime.py directly so it never surfaced). Fixed via remainingInstallerTimeoutMs(). - Classify an absent or forbidden runtime index (401/403/404/410) as OcrRuntimeNotPublishedError with a clear "Fast OCR still works" message instead of a raw HTTP 404, without retrying. Refs #552 |
||
|
|
5558cf18b8 |
docs(guide): describe both library save modes in database.md and architecture.md (#580)
Closes #578. Rewrites the user file library save-mode description in the English database.md and architecture.md guides (independent-new by default, parent-linked on overwrite) and updates all 20 translated copies of each, with i18n_source_hash re-stamped so the parity gate stays green. |
||
|
|
1113c761ea |
feat(library): wire save-mode into the five custom-client tool submitters (#577)
Closes #565. Wires the fileId/saveMode pair into the ocr, erase-object, remove-background, background-replace, and blur-background submitters so the library save-mode selector works for them; remove-background's two-phase effects route now auto-saves the final composite instead of the transparent intermediate. |
||
|
|
4fea434859 |
chore(landing): retire the 2.0 launch banner and refresh the social preview (#576)
Removes the temporary 2.0 launch banner and README note, and refreshes the social/OG card to the current landing hero (synced to landing/web/docs og-image.png). |