Adds a prefilled 'Request a tool' affordance to the home search empty state and beneath weak results. Opens the in-app feedback dialog with a new search_miss source and a structured search_query when analytics is on; links to a prefilled GitHub Discussions (Ideas) post when off, so a request is never silently dropped. Reuses the existing feedback pipe, dialog, and analytics gate; no new storage. i18n across all 21 locales.
Draw, type, or upload a signature and place resizable/rotatable copies across PDF pages; output flattened server-side with PyMuPDF. Visual electronic signature, not cryptographic. New interactive-sign display mode (pdf.js + Konva) and a custom docs-pool route.
PR #364 only changed en/nl; the appDescription and rotating-phrase tool count was still '240' (translated) in the other 19 locales (ar, de, es, fr, hi, id, it, ja, ko, pl, pt-BR, ru, sv, th, tr, uk, vi, zh-CN, zh-TW). Replaces the number in each; surrounding translations untouched. My earlier English-only grep missed these.
* 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.
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.
* 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.
* fix(docker): pin CUDA base to 12.6 so the GPU image starts on R560+ drivers
The amd64 base nvidia/cuda:12.9.2-cudnn-runtime bakes a cuda>=12.9 driver gate enforced by nvidia-container-toolkit at container start, so the image fails to launch on common production drivers (e.g. 570.x / CUDA 12.8). The AI bundles are all cu126 wheels and the image installs libcublas-12-6, so 12.9 was misaligned with the workload. Pin to nvidia/cuda:12.6.3-cudnn-runtime-ubuntu24.04 to match the wheels and lower the driver floor to R560+.
* fix(ai): broaden OOM detection so the rembg lighter-model fallback fires
onnxruntime/CUDA allocation failures surface as 'Failed to allocate memory for requested buffer', CUBLAS_STATUS_ALLOC_FAILED, or bad_alloc, not just 'out of memory'. The background-removal and transparency-fixer fallback-to-lighter-model paths only matched the literal 'out of memory', so the fallback was dead code and transparency-fixer (default birefnet-hr-matting) always failed with an allocation error. Add isMemoryAllocError() and use it in both checks.
* fix(ai): use bundled PaddleOCR models so OCR runs offline
ocr.py passed no model dirs to PaddleOCR, so PaddleX resolved models from ~/.paddlex and downloaded them from HuggingFace at runtime (slow first use, broken air-gapped), ignoring the models the OCR bundle ships in MODELS_PATH; it also pulled doc-orientation/unwarping models that are not bundled. Pin detection, recognition and textline models to the bundled dirs in MODELS_PATH (per language) and disable use_doc_orientation_classify / use_doc_unwarping, with per-component fallback when a model is absent. Verified: OCR runs with zero HuggingFace requests.
* fix(docker): add CAP_KILL so container shutdown is graceful
cap_drop: ALL without re-adding KILL meant tini (PID 1, root) could not forward SIGTERM to the gosu-dropped snapotter process (root minus CAP_KILL cannot signal a different UID). docker stop logged '[FATAL tini] forwarding signal: Operation not permitted', never delivered the signal, and fell back to SIGKILL after the 10s timeout. Add KILL to cap_add in both compose files. Verified: docker stop completes in 0s with SIGTERM delivered (exit 143) and no FATAL tini.
* fix(ai): serialize bundle installs against AI jobs to prevent sidecar segfault
A feature bundle install rewrites the shared Python venv (pip + copytree of site-packages/*.so) as a background subprocess, with no coordination against AI tool jobs that dlopen native libs (torch / onnxruntime CUDA) from the same venv; a job loading a shared object while it is overwritten segfaults the sidecar. Add a process-wide async mutex (venv-lock.ts): bridge.run() acquires it before every AI script and the install route holds it across the installer subprocess. Both run in the same Node process so a module-level lock suffices. Verified: concurrent install + AI job produces zero segfaults and the job serializes behind the install.
* fix(ai): make the venv lock read/write so concurrent AI jobs are not serialized
The first cut used an exclusive mutex, which (a) deferred the dispatcher spawn by a microtask and broke unit tests that synchronously drive the mocked spawn, and (b) serialized AI jobs against each other, removing the dispatcher's by-id request multiplexing. Make it a writer-preferring read/write lock: AI jobs are shared readers (with a synchronous fast path so spawn still happens in-tick) and a bundle install is the exclusive writer. Verified: all 764 AI unit tests pass.
* fix(ai): degrade OCR to Tesseract on CPU-only hosts instead of segfaulting
The amd64 AI bundle ships paddlepaddle-gpu, whose native libs dlopen
libcuda.so.1 at import and segfault on a host without a GPU (libcuda is the
driver lib, injected only by nvidia-container-toolkit on GPU hosts). The
segfault crashed the shared long-lived AI dispatcher and, after a few attempts,
tripped the bridge crash-recovery permanent-disable, wedging all AI until a
container restart. The standalone ocr tool defaults to quality=balanced
(PaddleOCR), so it hit this on every CPU-only deployment; ocr-pdf already
hardcoded Tesseract and was unaffected.
ocr.py now gates the PaddleOCR tiers on gpu_available(): balanced/best
transparently fall back to fast (Tesseract, CPU-capable) when no usable GPU is
present, and run_paddleocr_v5/run_paddleocr_vl refuse before importing paddle so
the GPU build is never dlopen'd on CPU. GPU hosts are unchanged.
Verified on a CPU-only Windows/WSL2 box: ocr returns Tesseract text across
repeated runs with the dispatcher staying healthy (no wedge).
The persistent Python dispatcher rejects scripts whose feature bundle is not
installed, but the per-request fallback (used when the dispatcher is down, e.g.
restarting right after a model repair) spawned scripts directly and bypassed
that gate. Behavior was therefore inconsistent: a gated script would fail under
the dispatcher but run under the fallback -- the "works once after a repair"
symptom from the original report.
- add packages/ai/src/feature-gate.ts: SCRIPT_BUNDLE_MAP + missingBundleForScript,
mirroring TOOL_BUNDLE_MAP in dispatcher.py, reading the same installed.json and
failing closed exactly like dispatcher._get_installed_bundles()
- runPerRequest now rejects with "feature_not_installed" (the same message the
dispatcher path surfaces) when a gated script's bundle is not installed
- unit tests for the gate, plus a drift test pinning the TS map to dispatcher.py
Closes#327
* fix(passport-photo): require the face-detection bundle, not just background-removal
Passport Photo runs face-landmark detection (face_landmarks.py, gated to the
face-detection bundle) before background removal (background-removal bundle),
but it was only declared under and guarded against background-removal. A user
who installed only Background Removal passed every JS-side check, then hit a
late "feature_not_installed" from the Python dispatcher gate when the analyze
step ran face landmarks, and the UI never told them Face Detection was needed.
- shared: add TOOL_EXTRA_BUNDLES + getRequiredBundlesForTool so a tool can
declare more than one required bundle (passport-photo needs background-removal
and face-detection). enablesTools is untouched, so the one-tool-per-bundle
invariant still holds.
- api: isToolInstalled() now checks every required bundle; add
getFirstMissingBundleForTool() so the analyze and base routes, pipeline (both
guards) and batch report the bundle the user actually still needs.
- web: the proactive install prompt (tool-page) and features-store treat a tool
as installed only when all required bundles are present, and point the prompt
at the first missing one (sequential install, no new UI).
Refs #327
* test(passport-photo): deterministic integration coverage for the two-bundle guard
Boots the real API with an isolated DATA_DIR and controls installed.json to
prove the HTTP route behavior end-to-end:
- nothing installed -> 501 naming background-removal
- only background-removal installed -> 501 naming face-detection (issue #327)
- both installed -> guard passes (not 501)
- base route reports face-detection too
Refs #327
* feat(web): add pure zoom/pan math module with unit tests
* feat(i18n): add a11y.pan key across all locales (English, matching adjacent zoom labels)
* feat(web): add useZoomPan hook (state + gestures over pure math)
* feat(web): add ZoomToolbar component
* feat(web): zoom & pan in the object eraser canvas
* feat(web): zoom & pan in the split tool preview
* fix(web): synchronous pan-mode refs so drag-pan is race-free under fast input
* test(e2e): zoom & pan acceptance (split always-on, eraser bundle-gated)
Translate the ~950 previously-untranslated Swedish UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated German UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Brazilian Portuguese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated French UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Spanish UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
The auth.rotatingPhrases and features.progressMessages arrays were skipped by
the main Italian pass (#298) and still had a few missing accents (e.g. verb
e -> è). Diacritic-only fix; wording, placeholders, and key parity unchanged.
Translate the ~950 previously-untranslated Thai UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Indonesian UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Vietnamese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Hindi UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Turkish UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Arabic UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Ukrainian UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Polish UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Russian UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Dutch UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Korean UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Japanese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Traditional Chinese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Translate the ~950 previously-untranslated Simplified Chinese UI strings (tool names,
descriptions, settings labels, dialogs) that were still showing English, and
restore the {size} placeholder dropped from settings.aiFeatures.diskUsage.
Machine-translated and verified: full key parity with en.ts, all {placeholders}
preserved, passes tsc and Biome. Native-speaker review welcome.
Around 35% of Italian strings (903 of 2583) were still English while the
file silently typechecked (key parity was already correct). This translates
them and fixes quality issues in the existing Italian:
- Translate untranslated tool names, descriptions, categories and UI labels;
keep legitimate English terms (formats, URL, Team, Pipeline, device presets)
- Fix accent errors (qualita->qualità, piu->più, Si e->Si è, and similar)
- Restore the {size} placeholder dropped from settings.aiFeatures.diskUsage
- Make role labels consistent Italian: Utente / Editore / Amministratore
- Normalize stray curly apostrophes to the file's straight-quote convention
Based on the Italian translation contributed by @albanobattistella (the issue
author), reconciled against the current en.ts (e.g. the Data->Files rename)
and corrected for accents and a structural error in the source.
Closes#231
* fix(deps): patch gray-matter onto js-yaml 4.2.0 (close js-yaml DoS alert)
js-yaml 3.14.2 (quadratic-complexity DoS in merge-key handling, GHSA
patched only in 4.2.0) was kept in the tree by a scoped pnpm override
"gray-matter>js-yaml": "^3.14.1" that exempted gray-matter from the
global js-yaml>=4.2.0 override. gray-matter is a build-time-only
transitive dep of the docs site (vitepress-plugin-llms,
@sugarat/theme-shared) and pinned 3.x because it calls the removed
yaml.safeLoad / yaml.safeDump APIs.
Remove the exemption so gray-matter resolves js-yaml 4.2.0, and add a
pnpm patch renaming safeLoad->load / safeDump->dump (the 4.x
equivalents; load is safe by default). js-yaml 3.x is now gone from the
lockfile.
Verified: gray-matter parse+stringify smoke test passes on 4.2.0; full
VitePress docs build green (177 pages, llms plugin parses all tool
frontmatter with no safeLoad/safeDump error).
* docs(ai): document rembg 2.0.69 pin and advisory non-reachability
The patched rembg 2.0.75 pulls a numpy 2.x closure (numpy>=2.3,
scipy>=1.16, scikit-image>=0.26) that is incompatible with the
numpy==1.26.4-locked AI stack (realesrgan 0.3.0 and codeformer-pip 0.0.4
break on numpy 2.x). Both open rembg advisories are unreachable in this
codebase: rembg is used purely as a library (never the `rembg s`
server), and new_session() only receives allowlisted model names
(remove_bg.py ALLOWED_MODELS), never user-controlled paths. Record this
rationale next to the pin; the Dependabot alerts are dismissed as
not_used.
- svg-sanitize.ts: strip each dangerous element repeatedly until stable with
whitespace-tolerant end tags, defeating nested/overlapping tags (closes 5
incomplete-multi-character-sanitization + 1 bad-tag-filter; the prior
single-pass regex could leave a residual <script>/<iframe>).
- file-preview.ts: add a resolve()+containment barrier (the path-traversal
guard CodeQL recognizes) on top of the id charset check (closes 9
path-injection).
- metadata.ts: bound the XMP namespace:name key segments so parseXmp cannot
backtrack polynomially (closes js/polynomial-redos).
- analytics-disabled.spec.ts: match analytics by URL host, not substring
(closes 4 incomplete-url-substring-sanitization).
typecheck + lint green; svg (119), preview (22), metadata (164) tests pass.
rembg 2.0.75 requires a numpy incompatible with the pinned numpy==1.26.4
that the rest of the ML stack (onnxruntime etc.) depends on, making
pip-audit's resolution impossible. The rembg <2.0.75 advisory (medium) is
accepted as a residual: it only affects the on-demand background-removal AI
bundle (publishing currently paused) and can't be patched without a numpy
2.x migration across the whole Python sidecar.
The fifth user-facing group is now Image, Video, Audio, PDF, Files
(internal modality id stays "file"; section.ts "files" was already
"Files"). Updates modality.ts label + comment, all 21 i18n locales
(categories.data "Data Files"->"Files", modalities.documentsAndFiles
"PDF & Data"->"PDF & Files", dead homePage.data), landing cards/hero
search/tools filter, docs headings, and e2e modality-tab assertions
(/^Data/ -> /^Files/, which had been failing).