The custom nav cluster (theme toggle + Fund + GitHub Star) rendered inline at
every width, overriding VitePress's responsive collapse: a horizontal scrollbar
at 768-959px and off-screen clipping of the buttons on 1280-1366px laptops.
Show the custom cluster only at >=1440px where it fits, defer to VitePress's
native nav below that, anchor the flyout menu to the start edge in RTL, and drop
the redundant "Home" nav link so the nav fits at 768px.
Closes#556
Anchor the Product menu panel below the navbar's bottom border line by bumping its top padding (pt-3 to pt-8). The panel was anchored to the button, landing 10px above the line, so the full-width navbar gradient line cut across the top of the open dropdown.
Editing a file from the library used to silently supersede it: the worker auto-saved every result as a new version and the leaf-only listing hid the original, which read as a destructive overwrite. Tool pages now show a per-edit choice for library-sourced files. The default saves the result as an independent new file and keeps the original; picking overwrite keeps the old superseding-version behavior.
The client sends a saveMode multipart field next to fileId, validated with a 400 on unknown values, and autoSaveToLibrary branches on it. Every hand-written route that honors fileId parses the field the same way as the factory. The review panel shows where an auto-saved result went instead of offering a second, duplicate save. Tools whose route or submitter ignores fileId keep the selector hidden via a shared unsupported-tools set, and the choice resets to the non-destructive default whenever a new file is staged.
Closes#495
Astro's getRelativeLocaleUrl lowercases the locale segment by default, so landing links and hreflang for zh-CN, zh-TW, and pt-BR were emitted lowercase and 404 on case-sensitive Cloudflare Pages. Pin the casing at the localizeHref chokepoint with normalizeLocale: false, add an e2e hreflang casing guard, and add a deploy-time check that blocks the build if any lowercased locale path leaks into the output.
Closes#554
Removes the client-side api.github.com fetch from the landing navbar and the docs theme; both now render a build-time star count (docs via a new VitePress data loader). deploy-docs.yml gets GITHUB_TOKEN plus a daily refresh cron, mirroring deploy-landing.yml. Fixes#555.
The Convert Audio tool promised configurable bitrate, sample rate, and channel count, but only format and bitrate were exposed. Adds an optional sampleRate setting (8000 to 96000 Hz, omitted = preserve source) wired through the Zod schema, the FFmpeg -ar flag, the standalone settings panel, and the pipeline builder controls.
Impossible combinations fail loudly instead of degrading silently: MP3 + 96000 Hz is rejected (libmp3lame caps at 48 kHz), and MP3 bitrates above the encoder ceiling at low rates (64 kbps at 8 kHz, 160 kbps at 16/22.05 kHz) are rejected rather than clamped. The UI offers only legal combinations and sanitizes stored pipeline settings on load.
Docs updated in English plus all 20 localized pages with refreshed i18n_source_hash stamps; two new UI strings added to all 21 locales.
Fixes#558
Tool-detail pages (/tools/<section>/<tool>/) and the /self-hosted pages are
built only in English, with no per-locale route, so a locale-prefixed link
404s in the static build. Add an enOnlyHref() helper and use it for those
links in Footer, Navbar, HeroSearch, and ToolGrid so localized pages point at
the English pages that actually exist. Adds an e2e guard asserting localized
pages emit un-prefixed URLs for those routes.
On mobile browsers 100vh is the tall viewport (URL bar retracted), so the
full-height shells (h-screen + overflow-hidden) rendered their bottom strip
below the visible area with no way to scroll to it. After an upload the tool
page's bottom control is the "Process" peek bar, so it landed off-screen on
every tool.
Switch the fixed-height shells from h-screen (100vh) to h-dvh (100dvh), which
tracks the visible viewport. AppLayout covers every tool page; the same sweep
covers the editor, login, change-password, 404, and the error/loading screens.
On desktop dvh equals vh, so desktop is unchanged. Adds a source-level guard
(tests/unit/web/app-shell-viewport.test.ts) since headless Chromium has no URL
bar and cannot reproduce the bug.
Independent reviewer agents checked all 20 locales of guide/low-resource plus the two cross-link paragraphs against the English source. 26 fixes across 12 locales (fr calques, tr/ru terminology drift, a zh-CN crop-vs-trim meaning error, a zh-TW garden-path sentence, ko particles, de collocations, es calques, small id/vi/th/sv fixes); 8 locales clean. Parity check and VitePress build pass.
Admin merge: docs-only PR, path-filtered required contexts never report (#420 precedent).
Comprehensive telemetry quality improvements across Sentry and PostHog, grounded in an audit of the live data plus current best-practice research.
Sentry: job_id/instance_id tags, operational fingerprinting, PII-safe settings context on bug events, web tag population + extension-noise filtering, an early-crash buffer, http status/method kept on breadcrumbs, and a gated-off-by-default performance-tracing re-enable (tracesSampler that zeroes db/redis/queue-poll root spans + drops the Redis integration) with worker job spans and canonical-host cron monitors.
PostHog: history_change SPA pageviews, instance_id super property for fleet rollups, enriched tool_used (formats, byte sizes, is_batch, execution_hint, real error_kind taxonomy), the previously-dead result_saved/batch_processed/ai_bundle_prompted events fired, search click-through, editor + Automate authoring + auth instrumentation, a before_send PII boundary, and minimal opt-in landing-site pageviews.
Localize the docs homepage (DocsHome.vue), top nav, and sidebar structural labels into all 20 non-English locales; individual tool names stay English by design. Also derive the homepage tool-count chips from the shared catalog via toolSection() so they no longer drift.
New guide/low-resource page: what runs well on 2 GB machines, a Raspberry Pi / old laptop Compose walkthrough with tuned caps, the env-var knobs that matter on small hardware, and what to skip. Linked from getting-started, the deployment hardware section, and the sidebar. Translated into all 20 non-English locales via the i18n batch pipeline; parity check and VitePress build pass.
Admin merge: docs-only PR, the path-filtered required integration contexts never report (#420 precedent).
Closes#497
A genuine pdf.js load failure (corrupt or password-protected file) was swallowed by the same catch that silences teardown rejections, leaving a blank canvas that looks like it is still loading. The two cases are now distinguished by the load effect's cancelled flag, and a real failure renders a clear message pointing at Unlock PDF for encrypted files. New loadFailed string in all 21 locales.
Item 4 of #478.
The password policy toggles (uppercase, lowercase, digit, special) can all be switched off in Settings -> Security, but the minimum-length input clamped at 4, so homelab admins couldn't deliberately allow short passwords. The API never enforced a floor; only the UI did. Lower the input floor to 1 and pin it with a test.
Closes#136
Briefly retry Postgres and Redis connectivity at boot (waitForService, DB_STARTUP_TIMEOUT_MS) so an ordered-but-not-yet-ready dependency recovers cleanly instead of crash-looping.
Every worker tool error now carries an input_format tag (file extension only, never the filename) for triage, derived once at the worker error path and added to the scrubber allowlist.
Postgres auth (28xxx), permission (42501), resource (class 53), and operator-intervention (class 57) failures now classify as operational via a cause-chain walk, not bug. pg query bugs (e.g. 42601) stay bugs.
Wrap convert and gif-tools process functions so a Sharp .toBuffer() failure carries an authored SafeError title (and the original as cause) rather than a scrubbed "Error: Error".
Fixes#533, found while working on #529/#531.
OIDC and SAML logins hard-blocked on the MFA policy with zero check of whether the user actually enrolled TOTP, and no challenge step at all. Once an admin turned on an MFA-required policy, every SSO user was permanently locked out regardless of enrollment status.
- Extract the post-auth MFA decision (challenge / enrollment-required / proceed) into a shared, unit-tested function so OIDC and SAML can't independently diverge again
- An already-enrolled user now gets a real challenge (reusing the existing, auth-method-agnostic MFA completion flow) instead of being blocked
- An unenrolled user under a required policy gets a distinct, correctly mapped error instead of the old generic one
- Fix a real fail-open regression caught in review: a transient DB error during the enrollment-status check could have silently skipped MFA entirely for an enrolled user; now it fails closed and logs
- Strip the one-time challenge token from the URL after consuming it
Fixes#529 (opened investigating #515).
Setting MFA policy to "required"/"admins only" saved regardless of whether the mfa enterprise feature was licensed, and there was no enrollment UI at all, so any instance that flipped the toggle locked every unenrolled user out with no way back in. The login page and Settings save also both collapsed the resulting error into a generic message, hiding the real reason.
- Reject saving mfaPolicy to admins_only/required server-side unless mfa is licensed
- Surface the specific server error on login and on a failed settings save instead of a generic fallback
- Add a self-service two-factor authentication enrollment flow (QR code, manual entry, recovery codes, verify, disable) so a licensed admin can actually satisfy the policy before it's enforced
- Fix a pending-enrollment dead end, silent error swallowing in verify/disable, and a silent clipboard-copy failure on the recovery codes screen
- Add the integration test that actually proves the fix: a real login attempt returns 403 MFA_ENROLLMENT_REQUIRED
Add a proportion chip row (Free, Original, 1:1, 4:3, 3:2, 16:9, 3:4, 9:16) to the Resize tool's Custom tab. Picking a ratio locks width and height so editing one recomputes the other, and prefills the largest box of that ratio that fits the source so it never upscales. Free stays the default, preserving existing behavior. Replaces the previously non-functional lock-aspect button. Frontend only, no backend or schema change; adds strings to all 21 locales.
Switch the footer LiftOff badge image to lift-off.sh's hosted webp so their launch-verification step passes. Removes the unused self-hosted copy; the do-follow link is unchanged.
Target-size compression had only a coarse DPI lever, so it undershot badly (a 350KB target could land at 216KB) and silently missed unreachable targets. Adds JPEG quality as a second lever (forced re-encode so it bites on JPEG scans), folds both into one monotonic quality axis that target-size binary-searches, reports targetMet honestly in the panel across 21 locales, and flips the tool to async for the extra passes. Quality-mode output sizes shift intentionally (slider now drives JPEG quality at full resolution in its top half).
Renames 18 ambiguous or hard-to-search tool names so image tools self-qualify like the other modalities ("Compress" becomes "Compress Image"), and cleans up a few awkward names. Propagated across search (constants.ts), display (en.ts + 20 locales), the OpenAPI base spec + 20 locale specs, and the docs tool-page headings in 21 languages. Removes the duplicate "Normalize Audio" summary shared by the video and audio endpoints. Tool ids and routes are unchanged, so no API paths or bookmarks break.
Extends the worker.ts timeout failure detail with actionable guidance (first-run model download, input too large for CPU inference, or busy/unavailable worker) while preserving the "Timed out after Ns" prefix so error_code classification, SSE terminal replay, and existing timeout assertions keep working. Adds a test assertion for the guidance.
Fixes#494
Add a once-per-boot instance_started event (arch, os, deploy_mode,
gpu_present) so the fleet architecture mix is measurable. It reuses the
existing per-instance instance_id and is exempt from the volume sample
rate, since a census that fires once per boot must not be thinned.
Restore useful capture depth now that the sponsored plan removes the
quota pressure behind the earlier hardening:
- PostHog sample rate 0.1 to 1.0 (full analytics when enabled); the
property allowlist still blocks file data.
- Sentry per-instance ceiling 20 to 500/hr, breadcrumb trail restored
(sanitized: urls/paths redacted, data payloads dropped), full stack
paths kept; local vars, request bodies, and PII still dropped. Both
api and web.
Honor ANALYTICS_ENABLED=false as an opt-out alias: it was documented on
the Docker Hub README but never wired in 2.x, so anyone who set it was
still tracked.
All capture stays behind the analytics opt-out gate.
Adds a Brush | Lasso toggle to the object eraser. Lasso lets the user drag a freeform loop that auto-closes and fills into the mask, so they select around a subject instead of painting every pixel. Frontend-only; the mask contract is unchanged. Also un-skips the erase-object e2e suite via a shared mockAiFeaturesInstalled helper (7 tests now run; 2 multi-file tests fixme'd for a pre-existing tool-page remount bug). Closes#492.
Adds a dedicated remove-gif-background AI tool: removes the background from an animated GIF, WebP, or APNG frame by frame and reassembles a transparent (or composited) animation in WebP, APNG, or GIF, with full per-frame effects. Reuses the background-removal bundle. Verified end-to-end with the real rembg model.
Closes#496.
Adds install method and hardest-setup-area as two optional questions to the first-login usage survey, so these fields are collected at onboarding scale instead of only via the low-traffic admin installer card. Both stay null until chosen and are omitted from the payload when unanswered. No API change and no new i18n; reuses the shared feedback schema and existing locale strings.
Follow-ups to the v2.1.0 Sentry telemetry overhaul, found by reviewing live release:2.1.0 events:
- error_code tag was empty because reportError read only the top-level err.code; add extractErrorCode() to walk the cause chain (pg SQLSTATE, node E-code, else first short code).
- InputValidationError from a tool's processV2 in the worker was logged as error_class=bug; classify it as expected for any source. Worker-side ZodError stays a bug (schema drift).
- AI dispatcher timeouts rejected with a bare Error, which the sanitizer scrubbed to a message-less "Error: Error"; reject with an operational SafeError (code "timeout") at both timeout sites.
Each fix written failing-test-first; affected and adjacent unit suites green plus full CI (integration + e2e).
The remove.bg comparison + tweaked alternatives rows, re-translated into all 20 languages but missed in the merge PR. Admin-merged (landing-only PR, skipped Integration matrix contexts never report; known repo deadlock). pnpm i18n:check passes on all 4 surfaces; landing build clean.
Render-blocking head script that routes a returning visitor to the language they explicitly picked (stored on switch). Only on default English pages; skips English-only tool-detail pages; crawler-safe. Admin-merged: landing-only PR, the skipped Integration matrix jobs never report the required contexts (known repo deadlock). Logic verified across 10 scenarios; landing build + typecheck clean.
All 181 docs markdown files translated into 20 languages (apps/docs/<locale>/**). Companion to the i18n code PR; admin-merged because the file count exceeds GitHub's per-PR CI trigger limit. Validated by pnpm i18n:check (all surfaces, 0 stale/missing) and a clean all-locale docs build.
Shared Claude Code translation pipeline (scripts/i18n, no API key) plus Astro/VitePress/Scalar i18n wiring. Landing and API reference translated into all 20 languages; docs i18n wiring + English source anchors. The translated docs markdown (apps/docs/<locale>/**, 3,620 files) follows in a companion PR because it exceeds GitHub's per-PR CI file limit.
The tool reference moved to /tools/<section>/<toolId>, but the five modality chips on the docs home still linked to the old flat paths. A full page load survives via the Cloudflare _redirects shim, but VitePress client-side navigation bypasses it and lands on the 404 page.
A dist-wide internal link check confirms these five were the only remaining stale links in the built site. The homepage e2e spec now pins all five chip hrefs and click-navigates one chip client-side, which is exactly the path that broke.
The repositioning in #469 pushed the full marketing pitch into the docs home hero: eyebrow badge, marketing h1, a bolded two-sentence value prop, and a second paragraph, all before the install command. The hero now states what the page is and gets out of the way: "SnapOtter Documentation" plus one line that keeps the self-hosted file-processing positioning, then the one-command install and quick links.
The Enterprise door CTA said "Evaluate for your org" but linked to /guide/architecture, already the first link in that same card. It now says "See enterprise features" and points at https://snapotter.com/enterprise, where the evaluation journey (features, pricing, demo booking) actually lives.
Also repairs the docs homepage e2e spec, which still asserted pre-2.0 copy and has been failing nightly since the hero changed, and adds coverage for both door CTAs.
Regenerate the social/OG card (200+ tools, Private file processing, self-hosted infrastructure) and sync to landing/web/docs; update banner, press kit, package + OpenAPI + Docker Hub descriptions, a leaked docs count, and the English About string.
Removes Sentry tracing entirely (BullMQ idle polling burned 4.8M transactions in 2 days at the baked 0.1 rate), decouples PostHog sampling, and replaces the type-only error scrub with a vetted-field sanitizer plus SafeError/ToolInputError contracts. One classified capture path with per-signature throttles and a per-process ceiling makes storms impossible (NODE-1E was 4,541 events from one 30s loop). Browser errors move to a dedicated web Sentry project with their own source maps. Adds the SNAPOTTER_TELEMETRY runtime kill switch and silences test fleets.
Crash fixes: remote 204/304 SSRF process kill (NODE-20), conversion-preset boot crash loop (NODE-21), Redis version preflight + unhandled subscribe rejection (NODE-1T), Sign PDF on plain-http origins (NODE-1K/1M), wavesurfer/pdf.js teardown rejections (NODE-1P/1N), bundle-import ZlibError to 400 (NODE-1Z), chart-maker input errors declassified (NODE-1H/1J), asset requests skip the session DB lookup (NODE-1D).
Adds a CSS-only Product dropdown (Developers, Self-hosted tools, Alternatives) to the top nav and restores the lg breakpoint. Mobile menu lists items flat. Updated homepage e2e (navbar + dropdown-hover tests, footer scope). Verified at 1024/1280px, 63/63 landing e2e.
Restores Developers to the top nav by swapping out the Features homepage anchor. Nav is now Enterprise, Pricing, Alternatives, Developers, Docs, Talk to a human. Updated two e2e assertions that checked for a Features nav link. Verified visually at 1120/1280px and 62/62 landing e2e.