Commit Graph
281 Commits
Author SHA1 Message Date
SnapOtterandGitHub 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
2026-07-21 16:48:48 +08:00
SnapOtterandGitHub 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.
2026-07-21 15:22:15 +08:00
SnapOtterandGitHub 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.
2026-07-20 18:02:39 +08:00
SnapOtterandGitHub 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.
2026-07-19 22:59:34 +08:00
SnapOtterandGitHub 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).
2026-07-19 21:23:48 +08:00
SnapOtterandGitHub 51022628dc fix(a11y): WCAG AA contrast retune for the Otter Orange palette (#567)
Fixes #557. Vivid fill, ink label: brand #E07832 stays on fills while primary-foreground flips to #1A1814 (5.83:1); new theme-aware ink tokens carry orange, destructive, and success text roles; opacity-modified text purged; landing, demo, and the docs fund button retuned. Guarded by a CSS-parsing unit contrast test, rebuilt axe baselines with zero contrast entries, a new landing axe smoke, and fully regenerated darwin visual baselines.
2026-07-18 12:56:48 +08:00
SnapOtterandGitHub 69f72a6c71 fix(docs): keep the nav within the viewport on tablets and laptops (#570)
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
2026-07-18 12:27:00 +08:00
SnapOtterandGitHub 54073a7c50 fix(landing,docs): use build-time GitHub star count, drop per-page api.github.com fetch (#560)
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.
2026-07-18 10:45:28 +08:00
SnapOtterandGitHub d4eaa655b2 fix(audio): expose sample rate setting in Convert Audio (#561)
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
2026-07-18 10:14:50 +08:00
SnapOtterandGitHub 8dcfd55745 docs(i18n): review pass over the low-resource guide translations (#549)
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).
2026-07-17 10:23:54 +08:00
SnapOtterandGitHub 9247947704 fix(docs): localize the docs homepage, nav, and sidebar chrome across 20 languages (#547)
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.
2026-07-17 09:18:26 +08:00
SnapOtterandGitHub fe85dd2b98 docs(guide): add a low-resource deployment guide in 21 languages (#548)
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
2026-07-17 00:40:48 +08:00
SnapOtterandGitHub f858c4cea0 feat: clearer, disambiguated tool names across all surfaces (#520)
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.
2026-07-15 21:55:51 +08:00
SnapOtterandGitHub 991c981529 fix: make OCR portable and reliable across AMD64 and ARM64 (#519)
* fix: make OCR portable and reliable

* fix: harden OCR installation portability

* fix: pin OCR partials across downloads

* fix: make OCR execution reliably asynchronous

* fix: harden OCR portability and docs routes

* fix: preserve decoder and docs safeguards
2026-07-15 03:34:24 +08:00
dependabot[bot]andGitHub 73e48c24d1 chore(deps): bump the production-deps group with 17 updates (#509) 2026-07-13 20:26:06 +08:00
dependabot[bot]andGitHub ec72ff100d chore(deps-dev): bump the dev-deps group with 10 updates (#510) 2026-07-13 19:52:48 +08:00
SnapOtterandGitHub cb5db59f77 feat(tools): remove background from animated GIFs (WebP, APNG) (#502)
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.
2026-07-11 19:53:00 +08:00
SnapOtterandGitHub 4963ab3bbd feat(docs-i18n): translate all documentation into 20 languages
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.
2026-07-11 13:52:47 +08:00
SnapOtterandGitHub 00b651c9f8 feat(i18n): 21-language pipeline, landing/docs/API wiring, landing+API translations
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.
2026-07-11 13:01:55 +08:00
SnapOtterandGitHub 4a5a6718e6 docs: point home modality chips at the sectioned tool pages (#485)
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.
2026-07-11 12:39:26 +08:00
SnapOtterandGitHub 4426c733e7 docs: declutter the docs home hero and fix the enterprise door CTA (#483)
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.
2026-07-11 11:43:08 +08:00
SnapOtterandGitHub d572b5c33c chore(branding): reframe assets to self-hosted infrastructure positioning (#479)
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.
2026-07-11 02:07:15 +08:00
semantic-release-bot c6baf6018d chore(release): 2.1.0 [skip ci]
# [2.1.0](https://github.com/snapotter-hq/snapotter/compare/v2.0.0...v2.1.0) (2026-07-10)

### Bug Fixes

* **demo:** boot to dashboard, sample-data notice, robust mobile editor icon ([#464](https://github.com/snapotter-hq/snapotter/issues/464)) ([a94b69b](https://github.com/snapotter-hq/snapotter/commit/a94b69b14f783c07abfe757e21314f580fbda556))
* **demo:** point banner "Self-host SnapOtter" link at the docs guide ([#466](https://github.com/snapotter-hq/snapotter/issues/466)) ([463ccff](https://github.com/snapotter-hq/snapotter/commit/463ccff5cb91c4b2d0f2c7309ea77789544ad2c3))
* **demo:** populate admin data, fix settings crash and mobile editor icon ([#463](https://github.com/snapotter-hq/snapotter/issues/463)) ([7329bc6](https://github.com/snapotter-hq/snapotter/commit/7329bc6a1334b925cf95384d6ddc172f04a86610))
* error-only Sentry telemetry, storm-proof capture, and crash fixes ([#476](https://github.com/snapotter-hq/snapotter/issues/476)) ([ae6a4c8](https://github.com/snapotter-hq/snapotter/commit/ae6a4c8b7c9275ccd8930251d429181568f83453))
* **landing:** clarify OIDC is free, SAML is enterprise on pricing ([#461](https://github.com/snapotter-hq/snapotter/issues/461)) ([c2c9a9e](https://github.com/snapotter-hq/snapotter/commit/c2c9a9ed9bea9c66efec84b9e0b9b2ebbf863073))
* **landing:** fix top nav overflow and text overlap ([#473](https://github.com/snapotter-hq/snapotter/issues/473)) ([ffeacd4](https://github.com/snapotter-hq/snapotter/commit/ffeacd4b3c7b5858d9048c2521569256e3569490))
* **landing:** green the landing e2e suite ([#470](https://github.com/snapotter-hq/snapotter/issues/470)) ([10ae6d1](https://github.com/snapotter-hq/snapotter/commit/10ae6d1d3d6ce8a527937bfa31e76ba392bc09e0))
* **landing:** highlight Open Source as Most Popular tier ([#462](https://github.com/snapotter-hq/snapotter/issues/462)) ([e468cef](https://github.com/snapotter-hq/snapotter/commit/e468cef6c0e28b4736dee32d2cf8c2ab1968a59c))
* **landing:** reposition residual meta framing and demote homepage tool count ([#471](https://github.com/snapotter-hq/snapotter/issues/471)) ([958c3e8](https://github.com/snapotter-hq/snapotter/commit/958c3e8513eb3a4c3be7488894981fc8794b1c5f))
* reliable, self-healing AI feature-bundle installs ([#472](https://github.com/snapotter-hq/snapotter/issues/472)) ([a731c3d](https://github.com/snapotter-hq/snapotter/commit/a731c3d1fe8303296e90cd138a01e903df871bb7))
* restyle sponsor button ([37a3cfc](https://github.com/snapotter-hq/snapotter/commit/37a3cfc84477c10831f9ae61f183f4ab4ef45d50))
* reword sponsor button to "Support us" ([#468](https://github.com/snapotter-hq/snapotter/issues/468)) ([331e3bf](https://github.com/snapotter-hq/snapotter/commit/331e3bfde04035151612275d9ae269909fed87a8))
* update python protobuf dependency ([780624e](https://github.com/snapotter-hq/snapotter/commit/780624e168cfda5abf3b2501455fe62674550fcf))

### Features

* **demo:** populate the Files library with sample files ([#465](https://github.com/snapotter-hq/snapotter/issues/465)) ([a6e8f75](https://github.com/snapotter-hq/snapotter/commit/a6e8f75074e38b8e5d856311d4423e8a936ae690))
* **landing:** add dismissible 2.0 launch banner ([ea7a671](https://github.com/snapotter-hq/snapotter/commit/ea7a671ccf29bcbab7a94c28efcbf6e1b9506b34))
* **landing:** add Product dropdown to nav ([#475](https://github.com/snapotter-hq/snapotter/issues/475)) ([3d1744a](https://github.com/snapotter-hq/snapotter/commit/3d1744aec84049f1f296f1cce8f6fb726cf7f835))
* **landing:** restore Developers nav link, swap out Features anchor ([#474](https://github.com/snapotter-hq/snapotter/issues/474)) ([a08afbc](https://github.com/snapotter-hq/snapotter/commit/a08afbc80afefb11cbc13e629d31225a6dceda2f))
* reposition to self-hosted file-processing infrastructure ([#469](https://github.com/snapotter-hq/snapotter/issues/469)) ([af011d5](https://github.com/snapotter-hq/snapotter/commit/af011d55381f8b57797984b1c5b2ca974a033d1c))
2026-07-10 13:55:46 +00:00
SnapOtterandGitHub ae6a4c8b7c fix: error-only Sentry telemetry, storm-proof capture, and crash fixes (#476)
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).
2026-07-10 21:41:49 +08:00
SnapOtterandGitHub a731c3d1fe fix: reliable, self-healing AI feature-bundle installs (#472)
Make on-demand AI feature-bundle installs reliable and self-healing, closing
the failure modes behind most "some tool doesn't work" reports.

Multi-bundle installs: tools needing more than one bundle (Passport Photo,
Enhance Faces) install every required bundle from one action and stay
not-installed until all are present. Verified across all 19 AI tools.

Downloads: self-heal the accelerated Hugging Face (Xet) client so an upgraded
venv no longer silently falls back to slow urllib; restart instead of
corrupting a resumed partial when a proxy ignores Range and returns 200;
verify the completed size; fail fast on disk-full and HTTP 4xx; retry
transient errors five times; add hf_transfer fallback and document Xet egress.

Install integrity: crash-atomic venv writes so a killed or out-of-space
install can no longer tear the shared venv and break other tools; a boot
breadcrumb reseeds a torn venv to a clean state automatically; a post-install
smoke import test refuses to record a bundle whose libraries cannot load; an
install watchdog stops a wedged installer that would otherwise hold the venv
writer lock forever.

Adds unit and end-to-end tests for every failure mode above.
2026-07-10 07:32:48 +00:00
SnapOtterandGitHub af011d5538 feat: reposition to self-hosted file-processing infrastructure (#469)
Leads public copy with self-hosted file-processing infrastructure and demotes tool count to a proof point across README, docs, llms.txt, DockerHub, and the landing site. Adds a /self-hosted hub plus 7 job-intent SEO pages with a build-time validator, deepens the flagship /alternatives pages, adds a remove.bg page, three shared components, and landing e2e coverage.
2026-07-10 13:26:41 +08:00
SnapOtter c1667f5b02 docs: mention analytics opt-out in getting started 2026-07-08 11:56:47 +08:00
SnapOtter f4da5ae3c7 docs: highlight layer-based image editor 2026-07-08 00:02:19 +08:00
SnapOtter 33f1f974f0 docs: add v2.0.0 changelog entry and refresh Docker Hub overview banner 2026-07-07 16:21:59 +08:00
SnapOtterandGitHub 60d01ab2dd fix: release-acceptance QA follow-ups (upload crash, scipy ABI conflict, rate limit, OCR fallback) (#458)
* fix(api): prevent a crash when an over-limit upload stream has no consumer yet

busboy's "limit" handler destroyed the file stream with an error but never
attached its own error listener, relying entirely on whatever consumes
part.file downstream to do so. On a fast enough connection (or a fully
buffered body, e.g. Fastify inject()), busboy can process enough bytes to
hit the size limit before the route handler's receiveUpload() call has
attached its own stream listener, leaving the resulting "error" event with
zero listeners -- which crashes the whole process by default in Node.

Surfaced by tonight's FULL_MATRIX+FUZZ integration run (880 uncaught
exceptions, all the same root cause). Reproduces deterministically in
isolation; unrelated to this release's actual code delta (file untouched
since PR #413, well before the baseline QA pass).

Fix: attach a baseline no-op error listener the moment the stream is
created, guaranteeing at least one listener always exists. EventEmitter
delivers "error" to every registered listener, so the real consumer's own
error handling is unaffected.

* fix(ai-bundles): rebuild upscale-enhance and photo-restoration to reconcile scipy ABI

upscale-enhance and photo-restoration both depend on codeformer-pip, whose
transitive closure (basicsr -> realesrgan -> gfpgan) pulls in an unpinned
scipy. Both bundles were last built ~June 18-19, before PR #437 added the
manifest's `constraints` array (numpy==1.26.4, scipy==1.12.0, etc.) to pin
exactly this kind of dependency during bundle builds. Only the ocr bundle
was rebuilt after that fix landed.

install_feature.py has no pip install step -- it's a raw tarfile extraction
with no cross-bundle conflict resolution, so installing OCR alongside either
stale bundle left three incompatible scipy versions' files mixed in the same
site-packages directory (a compiled _rotation.*.so from one release next to
Python files expecting a different release's API), breaking the `upscale`
tool and OCR's higher-quality tiers with an ImportError.

Rebuilt both bundles for amd64-gpu and arm64-cpu from the current manifest,
verified scipy/scikit-learn/scikit-image/pandas all resolve to the pinned
versions in the tarballs themselves, then verified end-to-end on real
hardware (Mac arm64 CPU and ubuntu_gpu .248 RTX 4070): installing all
affected bundles together now yields exactly one version of each constrained
package, `upscale` produces correct output, and OCR's balanced/best tiers
correctly use PaddleOCR-GPU instead of erroring out.

Published the rebuilt tarballs to the public deepsafe/feature-bundles
HuggingFace repo and updated this manifest's sha256/sizes to match.

Also adds verify-bundle-compatibility.sh: verify-bundle.sh checks each
bundle in isolation (a fresh venv per bundle), which is exactly why this
shipped twice -- nothing ever checked that bundles built at different times
agree once layered into the one shared venv real installs use. The new
script installs every bundle for an arch into one venv and asserts each
constrained package has exactly one, correct version.

Known follow-up (not fixed here, needs separate discussion): uninstalling a
bundle only removes its downloaded model weights, never the site-packages
it added, so existing installations that already hit this bug have no clean
self-service fix via uninstall+reinstall -- they need a full AI-venv wipe.

* fix(docker): bake a real rate limit default for the all-in-one one-liner

The documented single-container `docker run` install had RATE_LIMIT_PER_MIN=0
(effectively unlimited, ~50k/min) baked in, since only docker-compose.yml
carried a hardened override. A self-hoster following the one-liner path got
no meaningful throttling anywhere, including auth-adjacent routes with no
dedicated per-route limit. Bakes a generous-but-real 1000/min default into
the Dockerfile, raises both compose files' fallback to match so the two
documented install paths converge on the same posture, and updates the Zod
schema default plus docs that quoted the old value.

* fix(api): boot log undercounted tool routes by the conversion-preset total

The "Tool routes: N active" line logged before registerConversionPresets(app)
ran, so it only ever reported the base 158 tools, 83 short of the real
241-tool total. Presets have to register after the base loop (they delegate
to each base tool's own processV2), so the fix moves the log line to after
that call and has registerConversionPresets return its count instead of
reordering the dependency.

* fix(ai): forward {info}/{warning} stderr JSON instead of dropping it

The dispatcher stderr parser only recognized {ready} and {progress,stage}
shaped JSON lines; anything else that parsed as valid JSON (like ocr.py's
GPU-to-tesseract downgrade notice, an {"info": ...} line) matched neither
branch and fell through silently, never reaching docker logs. Adds explicit
{info}/{warning} handling that forwards to console.log/console.warn, same as
the existing [prefix]-tagged non-JSON path.

* fix(api): fall back to a lower OCR tier when PaddleOCR itself is unusable

ocr.ts already retries lower quality tiers on a crashed dispatcher, but the
condition only matched crash-style messages (segfault, exited unexpectedly).
ocr.py's own ImportError/exception handlers already produce messages telling
the caller to use a lower tier (e.g. on the scipy ABI conflict class of bug),
but nothing ever acted on them, so a broken PaddleOCR hard-failed with 422
instead of degrading to Tesseract like ocr-pdf effectively does. Broadens the
retry condition to also catch PaddleOCR-engine-unusable messages.

Note: ocr-pdf's tesseract-only behavior turned out to be an unrelated,
pre-existing, deliberate design choice (PaddleOCR segfaults on rasterized PDF
pages on arm64), not a graceful-fallback mechanism to copy -- the two tools
weren't actually solving the same problem, so this fixes ocr.ts's own gap
rather than trying to mirror ocr-pdf.
2026-07-07 12:18:28 +08:00
SnapOtterandGitHub 865c7789bf Fix stale image-only copy and add 1.x-to-2.0 migration guide (#454)
* docs: add 1.x-to-2.0 migration guide and upgrade notice

Adds MIGRATING.md with backup and upgrade steps, plus a short
"coming from 1.x?" callout in README and the docs upgrade guide
pointing existing users at it.

* fix: replace stale image-only and pre-rename data copy across product

SnapOtter grew from an image-only tool into a 5-modality suite
(Image, Video, Audio, PDF, Files), but copy in several places never
caught up. Fixes:

- dropzone.defaultFormats (i18n): every non-English locale still had
  the pure pre-2.0 image-only format list; English omitted Files
  entirely. Corrected across all 21 locales.
- settings.about.appDescription (i18n): "document, and data" workflow
  copy updated to "PDF, and file" across all 21 locales.
- constants.ts: Files category's raw name was still "Data Files".
- Landing hero subtitle, JSON-LD schema, llms.txt, and 7 spots in the
  competitor-comparison pages.
- Docs: VitePress config, supported-formats, deployment, and an
  architecture.md modality-naming nit.
- OpenAPI description, root package.json description/keyword, and a
  GitHub issue template dropdown option.

DOCKERHUB.md's separate "v1.x, image tools only" pre-release notice
is left untouched since 2.0 hasn't published to Docker Hub yet.

* test: update dropzone format-hint assertion to match corrected copy

The expected string still had the stale image-only/duplicated
PDF-Documents text from before the dropzone.defaultFormats fix.
2026-07-06 15:20:19 +00:00
SnapOtterandGitHub e09feadd68 docs: add 1.x to 2.0 migration guide pointer and community note (#448)
Root MIGRATING.md pointer + README upgrading link + 1.x community note across README/MIGRATING/docs; drop stale v2.0.0 callout.

Claude-Session: https://claude.ai/code/session_01P22pYJttXCe5cbCzRyfZac
2026-07-06 18:50:27 +08:00
SnapOtter d019217969 chore(deps): bump the production-deps group 2026-07-06 18:39:27 +08:00
SnapOtter 6e9933446e docs: sync api documentation 2026-07-06 08:10:36 +08:00
SnapOtterandGitHub 8574615ef5 docs(deployment): refresh hardware requirements from the 2.0 benchmark (#436)
Update the Hardware Requirements section with fresh six-machine benchmark data:
- Tiers corrected to the resource-sweep floor (2c/2G minimum; 512MB cannot start,
  1GB is single-file-only, batches need 2GB).
- Add the 64-bit-only architecture requirement (Pi 4/5 yes; 32-bit ARM and
  512MB boards no).
- Correct the AI-on-CPU viability (colorize/face-enhance are ~10s and usable, not
  "marginal to no") and add the AI RAM lever (~360MB idle without bundles vs
  ~2.6GB with all seven installed).
- Fix bogus GPU speedups (noise-removal/blur-faces are CPU-bound, ~1x, not
  13,400x/100x); real wins are upscale ~47x, face-enhance ~12x, transcribe ~4.5x,
  remove-bg ~4x; photo-restoration is CPU-bound even on a GPU.
- Call out video transcode as the one CPU-heavy tool; refresh concurrency numbers.

Claude-Session: https://claude.ai/code/session_01UvVCMNUBrgpghk8gye5gav
2026-07-05 20:19:24 +08:00
SnapOtterandGitHub dadf766899 fix(migrator): correct and harden the 1.x to 2.0 SQLite import (#434)
* feat(api): parse DATA_DIR from env for 1.x import auto-detection

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

* test(migrator): build 1.17.2 fixtures by replaying legacy migrations

Discovered the legacy migrations seed a Default team (0005) and builtin roles
(0007), so the replayed fixture carries them. Seed uses a distinct custom team.

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

* fix(migrator): self-adjusting column copy, jobs.status map, drop sessions, advisory lock

The importer now inserts only the intersection of source and live target columns,
so the three analytics_* columns 2.x dropped no longer break the first users INSERT
(and future dropped columns are handled generically). jobs.status is mapped onto the
2.x enum (error->failed). Sessions are no longer migrated. A pg_advisory_xact_lock
serializes concurrent replicas. Includes login-after-migrate and library assertions.

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

* test(migrator): CI drift guard fails when a required column is unfillable from 1.17.2

Introspects every NOT-NULL-no-default column of each migrated table in the current
schema and asserts the engine can fill it from a real 1.17.2 source. Turns a future
breaking schema change into a PR-time failure instead of a production import break.

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

* feat(migrator): orchestrator with detection, boot states, marker, blob count

sqlite-import.ts owns source resolution (explicit path, 'off' sentinel, DATA_DIR
probe), the four boot states (import/leftover/locked/none), the persisted
sqlite_import marker, and a read-only library-blob count. runBootImport wires them
together and catches TargetNonEmptyError as a benign multi-replica skip.

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

* feat(api): route boot through the 1.x import orchestrator; hide marker from non-admins

index.ts now calls runBootImport (which owns detection + the four boot states)
instead of the inline SQLITE_MIGRATE_PATH block. The sqlite_import marker is added
to SENSITIVE_KEYS (but not REDACTED_KEYS) so admins see the counts for the banner
while non-admins don't see the key at all.

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

* feat(migrator): add analyzeSqlite + dry-run/verify CLI

analyzeSqlite is a read-only pre-flight (no live Postgres): per-table row counts,
library-blob presence, and out-of-enum job statuses. The migrate:sqlite CLI now
lives in the orchestrator and supports --dry-run/--verify (prints the analysis and
exits without writing) alongside the existing import and --force.

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

* docs: add 1.x to 2.0 upgrade guide; fix volume-name casing

New apps/docs upgrade guide covering auto-detect, the SQLITE_MIGRATE_PATH override +
off opt-out, the dry-run, what carries over, locked-state recovery, and non-destructive
rollback. Leads with 'back up the WHOLE /data volume, not just snapotter.db' because
1.x WAL mode leaves data in snapotter.db-wal (surfaced by the real-image upgrade test).
Standardizes README/DOCKERHUB compose volume names on the canonical SnapOtter-data
casing so they match the repo compose and don't orphan an upgrader's volume.

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

* feat(web): admin 1.x migration banner + 21-locale strings

A one-time admin banner reads the sqlite_import marker from /v1/settings and shows
the import result (user + saved-file counts) on success, or a warning when a 1.x
database was found but not imported. Dismissal persists to a sqlite_import.dismissedAt
settings key. shouldShowMigrationBanner/parseMigrationMarker sit in feedback.ts with
the other shouldShow helpers; strings added to en.ts and all 20 other locales.

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

* style(landing): biome-format Hero.astro trustBadges array

Pre-existing formatting drift on main (its Lint check was skipped on the merge that
introduced it); this PR's full Lint run surfaced it. Formatting-only, applied via
the repo's own biome formatter to unblock the required Lint check.

Claude-Session: https://claude.ai/code/session_01721WHAUGxnVk22qEeTub7w
2026-07-04 15:15:39 +00: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 ec98dfde9a feat(landing,docs): SEO fixes and query-matched tool pages (#383)
Landing:
- Preload the Bricolage display font (LCP heading) to cut first paint
- Cap tool-page meta descriptions at ~160 chars via whole-sentence
  truncation, so SERP snippets no longer overflow or cut mid-phrase
- Drop dead HowTo + FAQPage JSON-LD from tool pages (Google removed
  HowTo rich results in 2023 and FAQ rich results in 2026); the visible
  content stays. Breadcrumb + WebApplication schema retained
- Shorten 22 tool titles that exceeded the ~60-char SERP limit
- Add an optional h1 override to tool SEO data
- Rewrite adjust-colors, smart-crop, and ringtone-maker to match real
  Search Console query intent (saturation; object-aware cropping;
  convert audio to ringtone), with query-matched H1s and FAQs

Docs:
- Add meta descriptions to 20 pages missing frontmatter

Repo:
- Ignore local credential JSON files (.secrets/, *client_secret*, *service_account*)
2026-07-01 16:20:05 +08:00
SnapOtterandGitHub 37dc0098ba docs: sync API and documentation coverage (#379)
* docs: sync API and docs coverage

* ci: pin pandoc for sandboxed conversions
2026-06-29 22:35:05 +08:00
SnapOtterandGitHub ef342c268b docs: clarify nvidia cuda ai support (#378) 2026-06-29 21:29:00 +08:00
SnapOtter 1d99acf9ee feat: merge PostHog customer feedback 2026-06-29 18:19:18 +08:00
SnapOtter 649e65b035 feat: add PostHog customer feedback 2026-06-29 18:16:33 +08:00
SnapOtter c6319cf8a9 fix(security): harden auth and outbound fetches 2026-06-29 17:54:12 +08:00
SnapOtterandGitHub 084a9c9faa feat: all-in-one embedded single-container mode (#377)
Embedded Postgres 17 + Redis via s6-overlay when DATABASE_URL/REDIS_URL are unset; restores the one-command docker run for 2.0. EMBEDDED=0 disables; Compose stays the production path. Verified arm64 (14/14 lifecycle + Compose regression) and amd64 (build + embedded smoke).
2026-06-29 15:49:09 +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 8f4235d2c6 fix(enterprise): ship enterprise package in prod image + S3, analytics, tracing, queue fixes (#342)
* fix(enterprise): ship enterprise pkg in prod image, full license features, tracing key fallback

docker/Dockerfile: COPY packages/enterprise manifest+src into the production stage.
Without it, apps/api's workspace link to @snapotter/enterprise dangles and every
import() throws (silently caught), so all 19 enterprise features failed closed
(enterprise.active=false) regardless of a valid license.

scripts/generate-license.mjs: sync PLAN_FEATURES with packages/enterprise/src/license.ts
so a --plan enterprise license unlocks all 19 features (was 8) and team unlocks 8.

apps/api/src/tracing.ts: accept SNAPOTTER_LICENSE_KEY as a fallback to LICENSE_KEY so
distributed_tracing activates with the same key as the rest of the app.

* fix(docker): keep scripts/bake-analytics.mjs in build context

.dockerignore excluded the whole scripts/ dir (PR #82, V1 hardening), but
docker/Dockerfile later added 'COPY scripts/bake-analytics.mjs' for the analytics
bake step. A clean production image build therefore fails with
'scripts/bake-analytics.mjs: not found'. The published image build is gated off in
CI so this latent break went unnoticed. Exclude scripts/* but re-include the one
file the Dockerfile needs.

* fix: S3 upload stream, analytics bake reaches API, dedupe retention field, reconcile orphan jobs

storage-s3.ts: wrap the upload AsyncIterable in Readable.from() so @aws-sdk/lib-storage
accepts it. STORAGE_MODE=s3 file uploads failed with 'Body Data is unsupported format'
for every tool because a bare async generator is not a Readable.

docker/Dockerfile: COPY the builder-baked analytics baked.ts into the API runtime stage.
The API re-copied the committed (off) baked.ts from the build context, so the
SNAPOTTER_ANALYTICS build arg had no effect on the API -- and since the SPA reads
/api/v1/config/analytics, analytics was off everywhere regardless of the arg.

settings-dialog.tsx: remove the duplicate tempFileMaxAgeHours control under Data
Retention; it bound the same setting key as the File Management control with a different
default, so editing either silently overwrote the other.

apps/api/src/index.ts: reconcile orphaned job rows (empty tool_id, never enqueued to
BullMQ) at boot so they don't sit in processing/queued forever and inflate the per-user
concurrent-job count and the upgrade-check in-flight gate.

* fix(web): style the SSO login buttons (they referenced undefined theme tokens)

The OIDC/SAML 'Sign in with <provider>' buttons used bg-secondary /
text-secondary-foreground, which the web theme never defines (it has primary,
background, foreground, muted, border, card, primary-subtle). Those classes resolved
to nothing, so the buttons rendered as bare unstyled text on the login page.

Restyle: the optional (non-enforced) buttons become white-card outline buttons with a
key icon and an orange hover tint, secondary to the primary Login button; the
SSO-enforced buttons become solid primary with the icon.

* fix: gate S3 behind license, custom-role enterprise perms, wire retention UI, cleanup

S3 is a licensed feature, but shipping packages/enterprise in every image removed the
implicit gate, so STORAGE_MODE=s3 worked without a license. Enforce
isFeatureEnabled('s3_storage') at boot and fail fast if unlicensed.

Custom roles can now be granted security:manage / compliance:manage / webhooks:manage
(roles.ts ALL_PERMISSIONS + the Roles UI) so admins can build least-privilege
compliance/security roles instead of only the built-in admin role.

retentionSweep now reads the jobsRetentionDays / auditRetentionDays DB settings the
System Settings UI writes (env vars become the fallback default), mirroring how the
temp-file sweep reads tempFileMaxAgeHours. Previously those two UI controls were no-ops.

Cleanup: drop the never-set snapotter_storage_bytes gauge and the unused
MAX_WORKSPACE_SIZE_GB env var; emit tool_client_error to PostHog from the web
ErrorBoundary (client crashes were not reaching analytics); add the Python
OpenTelemetry packages so the innermost sidecar.<script> span exports; fix the stale
'only local storage' line in the docs; delete two e2e-analytics specs that tested the
removed consent UI.

* fix(env): restore MAX_WORKSPACE_SIZE_GB default

security-auth-hardening.test.ts asserts env.MAX_WORKSPACE_SIZE_GB defaults to 10, so
the var is an intentional (tested) default, not dead code. Removing it in the cleanup
commit broke that unit test. Keep the declaration.
2026-06-24 17:27:59 +08:00
SnapOtterandGitHub b2104fcde6 chore: docs retention wording, demo analytics mock cleanup, gitignore fix (#343)
* docs: clarify file retention vs saved-library persistence

FILE_MAX_AGE_HOURS only affects unsaved processing results (raw uploads
and tool outputs). Files saved to the Files library persist until the
user deletes them. Spell that out in the configuration table and the
getting-started Files section.

* chore(demo): drop removed per-user analytics from the mock API

#336 deleted the per-user analytics consent system (the DB columns, the
PUT /api/v1/user/analytics endpoint, and the consent UI in apps/web). The
demo mock still simulated them: a PUT handler, the analyticsEnabled /
analyticsConsentShownAt state fields, and analytics fields on the session
user. Nothing reads them anymore, so remove them. The current
GET /api/v1/config/analytics mock stays (that endpoint still exists).

* fix(gitignore): anchor demo ignore to root so apps/demo stays trackable

A bare 'demo' pattern matched any dir named demo, including the tracked
apps/demo workspace. New files under apps/demo were silently ignored and
lint-staged choked re-adding it. Root demo/ (the intended target) no
longer exists; anchor the rule to /demo. Build artifacts under apps/demo
stay ignored via the global dist/ and .turbo/ rules.
2026-06-24 15:58:08 +08:00
SnapOtterandGitHub 5dac40d782 docs: detailed per-format tables for all modalities (#339)
Rewrite the video, audio, document, and file sections of the supported
formats page into per-format tables matching the image section (format,
extensions, codec/engine, notes), plus a dedicated subtitles table.

Also correct output claims to match the code: video output is MP4/MOV
(H.264) and WebM (VP9) with GIF/WebP from dedicated tools; audio output
is MP3, WAV, FLAC, OGG, M4A. Inputs verified against VIDEO_INPUTS,
AUDIO_INPUTS, DOCUMENT_INPUTS, and FILE_INPUTS.
2026-06-24 12:29:07 +08:00
SnapOtterandGitHub 607c24895b Improve docs search UX, dash cleanup, and colored modality tool icons (#338)
* feat(docs): improve search UX

- Hide the 'Search by Pagefind' branding in the search dialog footer
- Replace the 'No results found.' message shown before any query with a
  friendly hint (detected via empty-input :placeholder-shown state)
- Tune placeholder, empty-state, and results-heading copy; show a few
  sub-section matches per result (pageResultCount)

* docs: replace double-dash em-dash substitute with single dash

Swept prose ' -- ' to ' - ' and numeric ranges (e.g. 2--20 to 2-20)
across the documentation. CLI flags and code blocks left untouched.

* fix(landing): show colored category icons on modality tool pages

The /tools/<modality>/ pages rendered bare name+description cards with no
icon or color. Port the colored, category-tinted icon card from /tools/
so modality pages match the main catalog (icon, tint, 'Learn more').
2026-06-24 12:06:31 +08:00