mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
e09feadd6872e7cf8b78c18ed2dc53b8cdf803f3
1952
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
d019217969 | chore(deps): bump the production-deps group | ||
|
|
36dde9ad87 |
fix(ai): gate AI tools on per-framework GPU detection, not a shared boolean (#445)
gpu_available() answers "can ANY framework use a GPU" (torch, then ONNX, then
paddle). But torch tools consumed that shared boolean directly as
device = torch.device("cuda" if gpu_available() else "cpu"). On a GPU host where
gpu_available() is True via paddle or ONNX while torch is a CPU-only build, those
tools would route to a CUDA torch cannot use and crash. Transcription had the
mirror problem: it runs on CTranslate2 (not torch), so on a transcription-only
GPU box gpu_available() returned False and Whisper ran on CPU despite a GPU.
Add per-framework helpers to gpu.py:
- torch_gpu_available(): torch.cuda.is_available(), honoring SNAPOTTER_GPU.
- ctranslate2_gpu_available(): ctranslate2.get_cuda_device_count() > 0.
Point each tool at the helper for its own framework: upscale, noise_removal,
enhance_faces and restore use torch_gpu_available(); transcribe uses
ctranslate2_gpu_available(). ocr.py keeps gpu_available() (paddle-aware) and the
dispatcher keeps it for its startup GPU-status line. The SNAPOTTER_GPU override
check is factored into a shared _override_disables_gpu() helper.
TDD: 7 new tests in tests/test_gpu_detection.py cover both helpers (override,
CPU-only, absent framework), including the crux that torch_gpu_available() stays
False on a CPU-only torch build even when a GPU exists for another framework.
Claude-Session: https://claude.ai/code/session_01NfaRxjek8ex5nawvx3mVMf
|
||
|
|
bc59114dcb |
chore(deps-dev): bump the dev-deps group with 9 updates (#444)
Bumps the dev-deps group with 9 updates: | Package | From | To | | --- | --- | --- | | [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.1` | `2.5.2` | | [@semantic-release/github](https://github.com/semantic-release/github) | `12.0.8` | `12.0.9` | | [@testcontainers/postgresql](https://github.com/testcontainers/testcontainers-node) | `12.0.3` | `12.0.4` | | [@testcontainers/redis](https://github.com/testcontainers/testcontainers-node) | `12.0.3` | `12.0.4` | | [adm-zip](https://github.com/cthackers/adm-zip) | `0.5.17` | `0.5.18` | | [turbo](https://github.com/vercel/turborepo) | `2.10.0` | `2.10.3` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.1` | `4.3.2` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.1` | `4.3.2` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.3` | Updates `@biomejs/biome` from 2.5.1 to 2.5.2 - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.2/packages/@biomejs/biome) Updates `@semantic-release/github` from 12.0.8 to 12.0.9 - [Release notes](https://github.com/semantic-release/github/releases) - [Commits](https://github.com/semantic-release/github/compare/v12.0.8...v12.0.9) Updates `@testcontainers/postgresql` from 12.0.3 to 12.0.4 - [Release notes](https://github.com/testcontainers/testcontainers-node/releases) - [Commits](https://github.com/testcontainers/testcontainers-node/compare/v12.0.3...v12.0.4) Updates `@testcontainers/redis` from 12.0.3 to 12.0.4 - [Release notes](https://github.com/testcontainers/testcontainers-node/releases) - [Commits](https://github.com/testcontainers/testcontainers-node/compare/v12.0.3...v12.0.4) Updates `adm-zip` from 0.5.17 to 0.5.18 - [Release notes](https://github.com/cthackers/adm-zip/releases) - [Changelog](https://github.com/cthackers/adm-zip/blob/master/history.md) - [Commits](https://github.com/cthackers/adm-zip/compare/v0.5.17...v0.5.18) Updates `turbo` from 2.10.0 to 2.10.3 - [Release notes](https://github.com/vercel/turborepo/releases) - [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md) - [Commits](https://github.com/vercel/turborepo/compare/v2.10.0...v2.10.3) Updates `@tailwindcss/vite` from 4.3.1 to 4.3.2 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-vite) Updates `tailwindcss` from 4.3.1 to 4.3.2 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss) Updates `vite` from 8.0.16 to 8.1.3 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.1.3/packages/vite) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.5.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: "@semantic-release/github" dependency-version: 12.0.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: "@testcontainers/postgresql" dependency-version: 12.0.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: "@testcontainers/redis" dependency-version: 12.0.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: adm-zip dependency-version: 0.5.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: turbo dependency-version: 2.10.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: tailwindcss dependency-version: 4.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: vite dependency-version: 8.1.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
eb40e68274 |
chore(deps): bump the actions group with 8 updates (#442)
Bumps the actions group with 8 updates: | Package | From | To | | --- | --- | --- | | [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.1.0` | `4.2.0` | | [docker/login-action](https://github.com/docker/login-action) | `4.2.0` | `4.4.0` | | [github/codeql-action/init](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` | | [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` | | [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` | | [docker/metadata-action](https://github.com/docker/metadata-action) | `6.1.0` | `6.2.0` | | [docker/build-push-action](https://github.com/docker/build-push-action) | `7.2.0` | `7.3.0` | | [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` | Updates `docker/setup-buildx-action` from 4.1.0 to 4.2.0 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5...bb05f3f5519dd87d3ba754cc423b652a5edd6d2c) Updates `docker/login-action` from 4.2.0 to 4.4.0 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/650006c6eb7dba73a995cc03b0b2d7f5ca915bee...af1e73f918a031802d376d3c8bbc3fe56130a9b0) Updates `github/codeql-action/init` from 4.36.2 to 4.36.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a) Updates `github/codeql-action/autobuild` from 4.36.2 to 4.36.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a) Updates `github/codeql-action/analyze` from 4.36.2 to 4.36.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a) Updates `docker/metadata-action` from 6.1.0 to 6.2.0 - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9...dc802804100637a589fabce1cb79ff13a1411302) Updates `docker/build-push-action` from 7.2.0 to 7.3.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/f9f3042f7e2789586610d6e8b85c8f03e5195baf...53b7df96c91f9c12dcc8a07bcb9ccacbed38856a) Updates `github/codeql-action/upload-sarif` from 4.36.2 to 4.36.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: docker/login-action dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: github/codeql-action/init dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: github/codeql-action/autobuild dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: github/codeql-action/analyze dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: docker/metadata-action dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: docker/build-push-action dependency-version: 7.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
3aaaacc7a1 |
feat: pin frequently-used tools to the top of the dashboard (#440)
* feat(i18n): add pin/unpin/pinned strings, retire addToFavourites stub Claude-Session: https://claude.ai/code/session_01Ad5LjCDJyW1tLFd3P4Hedp * feat(web): add per-user pinned-tools store Claude-Session: https://claude.ai/code/session_01Ad5LjCDJyW1tLFd3P4Hedp * feat(web): add opt-in pin toggle to ToolCard Claude-Session: https://claude.ai/code/session_01Ad5LjCDJyW1tLFd3P4Hedp * feat(web): render Pinned section on the dashboard All tab Claude-Session: https://claude.ai/code/session_01Ad5LjCDJyW1tLFd3P4Hedp * test(web): cover pin toggle (component) and dashboard pin flow (e2e) Claude-Session: https://claude.ai/code/session_01Ad5LjCDJyW1tLFd3P4Hedp |
||
|
|
ec78d36d95 | docs: merge api sync documentation branch | ||
|
|
6e9933446e | docs: sync api documentation | ||
|
|
b69c7362e1 | docs: sync api documentation | ||
|
|
7cd514dd8c |
fix(ai): detect a paddle-only GPU so OCR uses PaddleOCR-GPU not Tesseract (#439)
gpu_available() probed torch, then ONNX Runtime, then nvidia-smi, but never paddle. The OCR bundle ships paddlepaddle-gpu with no torch or ONNX, so on an OCR-only GPU host every probe missed the GPU: nvidia-smi saw it but returned False by design, and OCR silently fell back to Tesseract (CPU, lower quality) with no signal why. Add a paddle probe as the last resort in gpu_available(). It runs only after nvidia-smi confirms a GPU is physically present, and in an isolated subprocess, because importing paddlepaddle-gpu on a GPU-less host segfaults and would wedge the shared AI dispatcher. It returns True only when paddle reports both a CUDA build and a visible device, signalling the result through the exit code so paddle's own import chatter on stdout cannot corrupt the reading. CPU-only and torch/ONNX GPU hosts are unaffected: the probe never runs on the former (nvidia-smi finds nothing) and is never reached on the latter (the torch step already returns True first). Claude-Session: https://claude.ai/code/session_01NfaRxjek8ex5nawvx3mVMf |
||
|
|
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 |
||
|
|
fd39f66f46 |
fix(ai-bundles): lock the numpy-1.x ABI closure so the OCR bundle can't strand scipy (#437)
* fix(ai-bundles): lock the numpy-1.x ABI closure so the OCR bundle can't strand scipy The OCR bundle installs paddleocr[doc-parser] 3.4, whose dependency closure drags numpy 1.26.4 up to 2.5.1 and pulls scipy/scikit-learn/pandas wheels built against the numpy 2.x ABI. build-bundle.sh re-pinned only numpy (basePackages), so those numpy-2.x wheels stayed behind; the by-dir-name site-packages diff then shipped them, and once merged onto the numpy==1.26.4 base they raise "numpy.dtype size changed" on import. Because the dispatcher pre-imports every ML library at startup and disables all AI after 5 crashes in 60s, one stranded scipy takes down every AI tool, not just OCR (observed on a CPU host: remove-background worked before the OCR bundle and broke after). All-7 installs escaped it through last-writer-wins ordering; a subset install did not, which is why it surfaced only intermittently. Fix: add a manifest "constraints" list (numpy, scipy, scikit-learn, scikit-image, pandas pinned to numpy-1.x-ABI versions) and apply it via PIP_CONSTRAINT to every bundle pip install, so no bundle can pull a numpy-2.x wheel. paddleocr 3.4.1 still resolves cleanly under the lock and the pinned stack imports without ABI error on numpy 1.26.4 (validated on py3.12). Also import scipy/sklearn in the OCR path of verify-bundle.sh so CI catches this class in isolation, and add a manifest regression test. Note: the published bundles must be rebuilt and republished (ai-bundles.yml) for this to reach already-installed bases. Claude-Session: https://claude.ai/code/session_01UvVCMNUBrgpghk8gye5gav * chore(ai-bundles): sync OCR manifest sha256 to the rebuilt numpy-1.x bundles Rebuilt the OCR bundle for both arches with the numpy-1.x-ABI constraints from this PR and republished the tars to deepsafe/feature-bundles/v2.0.0, then updated the baked manifest sha256 and sizes so installs verify against the fixed archives: amd64-gpu 5.93 GB sha 2a00a3184f6a635f1fa9ae2a6517ad740a11f9e5ff58c098d2fd369a2bb1e16b arm64-cpu 1.98 GB sha 6868c264069dcb74c6675c0b1f58dc1c9f60d9aa4459725e3dbde07a99a6a09a Both tars ship scipy 1.12.0 / scikit-learn 1.4.2 / pandas 2.2.2 (numpy-1.x-ABI) and zero numpy-2.x wheels, verified by listing the archive contents. Stopgap note: these tars were built against the ghcr.io latest base (the 2.0.0 image is not published to GHCR), so they are not byte-identical to what the CI build will produce. When ai-bundles.yml rebuilds at the 2.0.0 release, it will mint fresh sha256 values and this manifest must be re-synced to them. Claude-Session: https://claude.ai/code/session_01UvVCMNUBrgpghk8gye5gav |
||
|
|
48494afc1e |
chore(ai-bundles): sync OCR manifest sha256 to the rebuilt numpy-1.x bundles
Rebuilt the OCR bundle for both arches with the numpy-1.x-ABI constraints from this PR and republished the tars to deepsafe/feature-bundles/v2.0.0, then updated the baked manifest sha256 and sizes so installs verify against the fixed archives: amd64-gpu 5.93 GB sha 2a00a3184f6a635f1fa9ae2a6517ad740a11f9e5ff58c098d2fd369a2bb1e16b arm64-cpu 1.98 GB sha 6868c264069dcb74c6675c0b1f58dc1c9f60d9aa4459725e3dbde07a99a6a09a Both tars ship scipy 1.12.0 / scikit-learn 1.4.2 / pandas 2.2.2 (numpy-1.x-ABI) and zero numpy-2.x wheels, verified by listing the archive contents. Stopgap note: these tars were built against the ghcr.io latest base (the 2.0.0 image is not published to GHCR), so they are not byte-identical to what the CI build will produce. When ai-bundles.yml rebuilds at the 2.0.0 release, it will mint fresh sha256 values and this manifest must be re-synced to them. Claude-Session: https://claude.ai/code/session_01UvVCMNUBrgpghk8gye5gav |
||
|
|
67c55669d6 |
fix(ai-bundles): lock the numpy-1.x ABI closure so the OCR bundle can't strand scipy
The OCR bundle installs paddleocr[doc-parser] 3.4, whose dependency closure drags numpy 1.26.4 up to 2.5.1 and pulls scipy/scikit-learn/pandas wheels built against the numpy 2.x ABI. build-bundle.sh re-pinned only numpy (basePackages), so those numpy-2.x wheels stayed behind; the by-dir-name site-packages diff then shipped them, and once merged onto the numpy==1.26.4 base they raise "numpy.dtype size changed" on import. Because the dispatcher pre-imports every ML library at startup and disables all AI after 5 crashes in 60s, one stranded scipy takes down every AI tool, not just OCR (observed on a CPU host: remove-background worked before the OCR bundle and broke after). All-7 installs escaped it through last-writer-wins ordering; a subset install did not, which is why it surfaced only intermittently. Fix: add a manifest "constraints" list (numpy, scipy, scikit-learn, scikit-image, pandas pinned to numpy-1.x-ABI versions) and apply it via PIP_CONSTRAINT to every bundle pip install, so no bundle can pull a numpy-2.x wheel. paddleocr 3.4.1 still resolves cleanly under the lock and the pinned stack imports without ABI error on numpy 1.26.4 (validated on py3.12). Also import scipy/sklearn in the OCR path of verify-bundle.sh so CI catches this class in isolation, and add a manifest regression test. Note: the published bundles must be rebuilt and republished (ai-bundles.yml) for this to reach already-installed bases. Claude-Session: https://claude.ai/code/session_01UvVCMNUBrgpghk8gye5gav |
||
|
|
47a60e7fad |
fix(files): record the source tool in toolChain on Save to Files (#435)
Save to Files posted only the blob, so userFiles.toolChain stayed null and the library showed "Tools Used: None". Thread the producing toolId through /api/v1/files/upload (validated optional field) and store it as a one-element toolChain, matching the pipeline path. Claude-Session: https://claude.ai/code/session_01UvVCMNUBrgpghk8gye5gav |
||
|
|
cf884b52cd |
fix: offline CodeFormer face-enhance (ship RealESRGAN_x2plus in upscale-enhance bundle) (#433)
* fix: ship RealESRGAN_x2plus.pth in the upscale-enhance bundle for offline CodeFormer codeformer-pip 0.0.4 downloads RealESRGAN_x2plus.pth at import of codeformer.app, unconditionally, even though enhance_faces calls inference_app with background_enhance=False and never uses the background upsampler. The weight was not bundled, so explicit CodeFormer face-enhance (enhance-faces model=codeformer) failed in strict offline mode (SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0) on a host that had never cached it -- the guard raised before the import could complete. Add RealESRGAN_x2plus.pth to the upscale-enhance bundle manifest (only that bundle uses codeformer-pip; photo-restoration uses the CodeFormer ONNX path) and link it in prepare_codeformer_weights alongside the other three weights, replacing the download-or-error guard. Once the bundle ships it, the import resolves offline and strict mode works. Archive SHA256s updated in a follow-up once the bundle is rebuilt. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 * fix: require face-detection bundle for enhance-faces + point manifest at the x2plus archives enhance-faces runs MediaPipe face detection (blaze_face_short_range.tflite) before CodeFormer/GFPGAN. That model ships in the face-detection bundle, not the tool's primary upscale-enhance bundle, so a standalone upscale-enhance install failed face detection (offline: hard error; online: a surprise download) before reaching the codeformer path. Declare the dependency in TOOL_EXTRA_BUNDLES like passport-photo does. Update the upscale-enhance archive SHA256/sizes to the rebuilt bundles that include RealESRGAN_x2plus.pth (amd64-gpu + arm64-cpu), verified to install and run enhance-faces model=codeformer in strict offline mode with zero downloads. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 |
||
|
|
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 |
||
|
|
dc589fd0a1 |
docs(readme): drop Otter.ai from the alternatives list (#432)
Otter.ai is a transcription-only service and reads as off-topic next to the file-conversion tools in the comparison line. Remove it and fix the list conjunction. Claude-Session: https://claude.ai/code/session_01EhgRGhvhNGJVQHcdFdcNMa |
||
|
|
8451f9be08 |
feat(landing): refocus hero on privacy-sensitive teams (#431)
Retitle the homepage hero to "File processing for privacy-sensitive teams", drop the redundant infrastructure subline, and trim the trust badges to the four foundational signals (self-hosted, open source, air-gap capable, compliance-friendly). Align the homepage title and meta description to the new positioning, overriding the site-wide defaults for the homepage only. Claude-Session: https://claude.ai/code/session_01EhgRGhvhNGJVQHcdFdcNMa |
||
|
|
8cdd85a493 |
feat(feedback): route failed-run Report issue through the offline handoff (#429)
Apply the always-on handoff to the failed-run Report issue button too. Un-gate the two buttons in tool-page.tsx from the analytics toggle, and extend the dialog offline handoff to source=failed_job, prefilling the GitHub issue with the tool id and error category so it is actionable even with an empty message. Follows #428. Claude-Session: https://claude.ai/code/session_01XVrHKXwzZDWBWgkGQdPZ3A |
||
|
|
e0dbf2a5c3 |
fix: set a writable HOME for the app user so PaddleOCR works in non-root deployments (#430)
The container dropped privileges to the non-root snapotter user via gosu
(external) and s6-setuidgid (embedded), both of which preserve the
environment without setting HOME. The app therefore kept root's HOME=/root,
which is not writable by snapotter, and PaddleOCR died with
PermissionError: '/root/.paddlex/temp' -- breaking the ocr tool at default
quality in every non-root deployment. Prior GPU QA ran the app as root, which
masked it.
Fix: export HOME=/data/.home (persistent, writable, hidden) at every
privilege-drop point:
- entrypoint.sh external gosu path and non-root tini path (the latter uses
$DD/.home so a DATA_DIR override stays consistent).
- the s6 snapotter/run service (scoped there, not globally before /init, so
postgres/redis do not inherit a snapotter-owned HOME).
The root preflight creates /data/.home and the existing chown sweep owns it as
the PUID/PGID-remapped snapotter; the dir is added to both ensure_writable
probes so an unwritable HOME fails fast with the storage-permission guidance
instead of crashing late. The Dockerfile passwd home moves from /app
(read-only) to /data/.home as the getpwuid fallback when HOME is unset.
Because bridge.ts forwards HOME to the Python sidecar, this also repairs the
expanduser("~") caches in inpaint/outpaint/restore/noise_removal/remove_bg,
not just PaddleOCR.
Also fixes a test-harness inconsistency: tool-default-settings passport-photo
countryCode "us" -> "US" (the route exact-matches uppercase PASSPORT_SPECS
codes; the UI already sends "US", so users were never affected).
Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7
|
||
|
|
5dcc06a99e |
feat(feedback): always-on nav button with GitHub/email handoff when analytics is off (#428)
Keep the top-nav feedback button always visible (icon plus label on desktop, icon-only on mobile) instead of hiding it when an instance opts out of analytics. When analytics is off, the dialog keeps the typed message and hands off to a prefilled GitHub issue plus a contact@snapotter.com email, with no fake Thanks. Adds a feedback.yml issue template, URL builders, and feedback strings across all 21 locales. Claude-Session: https://claude.ai/code/session_01XVrHKXwzZDWBWgkGQdPZ3A |
||
|
|
7b04317ed2 |
feat: add a Keep it free sponsor button to the top nav (#427)
Adds a prominent Keep it free sponsor button to the top nav, linking to https://github.com/sponsors/snapotter-hq. Solid orange pill on desktop (left of the avatar), orange heart icon on mobile. Opens in a new tab with rel=noopener noreferrer, so no referrer or user data leaks, and it adds no passive network activity (offline-mode compatible). Fires an opt-in, property-less sponsor_clicked analytics event. Adds sidebar.sponsor and a11y.sponsorLink across all 21 locales. Claude-Session: https://claude.ai/code/session_01DnYLLA5z4Uf1GDeEPENVgr |
||
|
|
3ae48cc76a |
fix(i18n): complete the Italian feedback translations (#426)
Lands albanobattistella's Italian translation of the feedback strings from #425, with two mistyped keys corrected (great and adminCardDescription). Verified against main: typecheck 0 errors, i18n parity 23/23, Biome clean. Co-authored-by: albanobattistella <34811668+albanobattistella@users.noreply.github.com> |
||
|
|
23efce9df0 |
fix(analytics): harden analytics opt-out and feedback surfaces (#423)
Server stops phoning Sentry home after opt-out (release-health sessions + client reports off); settings saves diff-send only changed keys so a stale tab cannot revert an instance-wide opt-out; disabling analytics hides the feedback UI immediately; optIn resumes PostHog after re-enable; onboarding survey writes time out at 15s; inline tool-feedback prompt arms a shown-cooldown. |
||
|
|
6e3a14ec6b |
fix: remove automatic third-party egress of user data + optional strict offline mode (OSM tiles, Scalar fonts, editor fonts, AI model downloads) (#422)
* fix: remove all automatic third-party egress (OSM tiles, Scalar fonts, editor Google Fonts, AI model download fallbacks) Phone-home audit follow-up. The product no longer makes any automatic third-party request; user-initiated click-outs stay, and production now fails closed on missing AI models. 1. GPS leak via OSM tiles: the strip-metadata panel auto-loaded tile.openstreetmap.org tiles encoding the photo's GPS position. The Leaflet mini-map is gone; coordinates render as text plus an explicit View on map link (openstreetmap.org, opens on click only). Removed tile.openstreetmap.org from the CSP img-src, dropped the leaflet dependency, added the viewOnMap i18n key to all 21 locales. 2. Scalar docs fonts: /api/docs loaded Inter and JetBrains Mono from fonts.scalar.com. Scalar now renders with withDefaultFonts: false and both --scalar-font and --scalar-font-code pinned to system stacks; fonts.scalar.com removed from the docs CSP font-src. Verified by injecting GET /api/docs/: config carries withDefaultFonts false and the served page has no fonts.scalar.com reference. 3. Editor Google Fonts: the editor font picker built fonts.googleapis.com stylesheet URLs for 25 web fonts the served CSP already blocked. The remote loading path is deleted; the picker now offers system fonts only, with a SELF_HOSTED_FONTS seam (FontFace API, same origin) for bundling fonts later. Unknown families saved in old documents fall back to the browser default. 4. Python sidecar fails closed on model downloads: new packages/ai/python/offline_guard.py gates every runtime download fallback (inpaint, outpaint, restore, noise_removal, detect_faces, enhance_faces, face_landmarks, red_eye_removal, remove_bg, ocr, transcribe, upscale) behind SNAPOTTER_ALLOW_MODEL_DOWNLOAD=1 with an actionable error. Bundled models keep working untouched. 5. OCR and transcription library-internal downloads: unbundled PaddleOCR language and detection fallbacks now raise the guard error naming the language instead of resolving models over the network; faster-whisper gets local_files_only when downloads are off. 6. GFPGAN and CodeFormer cwd-relative weights: facexlib and codeformer-pip resolve helper weights relative to the process cwd and fetch them from GitHub when absent. They are now symlinked from the installed bundle files under MODELS_PATH/gfpgan/facelib before the libraries load, failing closed when unresolvable. Defense in depth: HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 are set in the runtime image and in the sidecar spawn env; install_feature.py lifts them for user-initiated bundle installs and restores them afterwards (it can run in-process inside the dispatcher). SNAPOTTER_ALLOW_MODEL_DOWNLOAD is documented in .env.example, default off. Validation: typecheck 9/9 workspaces, Biome clean on touched files, 5178 unit tests pass, py_compile on all touched scripts, guard behavior exercised in both dispatcher exec and per-request import modes, zero remaining runtime references to the three hosts. Docker build and live AI inference need post-merge verification on the GPU host. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 * fix: allow AI model downloads by default, make strict offline mode opt-in Product call: ease of use first. The download gating from the previous commit inverts its default: runtime model fetches (public model weights only, never user data) are allowed out of the box so AI tools self-heal, and SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0 becomes the explicit strict offline mode for airgapped deployments, where every fallback raises the actionable error instead of fetching. Changes: offline_guard blocks only on an explicit 0/false; the unconditional HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE image ENV is removed and bridge.ts sets those flags for the sidecar only in strict mode; .env.example documents the new default; install_feature's lift/restore stays. All bundled-path preferences, pre-existence checks, and symlink pre-placement remain, so installed bundles never trigger a download. The OSM, Scalar font, and editor font fixes are unchanged. Validation rerun: typecheck 9/9, Biome clean on touched files, 5178 unit tests pass, py_compile on touched scripts, guard behavior verified for unset/1 (allowed) and 0/false (blocked with the new message). Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 |
||
|
|
7b6765030b | ci: gate demo deploy | ||
|
|
f6f7b5a4bc | fix: sync demo theme with app | ||
|
|
7e6939bf0d |
docs: show "200+" for the landing tool-count stat (#420)
Match the drift-proof public phrasing used elsewhere (the Languages stat already reads "20+"). Avoids the below-hero number going stale as tools are added. Claude-Session: https://claude.ai/code/session_01JQ8LmV8LPLi8yNTayHzSTQ |
||
|
|
7473d2d0fc | ci: deploy demo automatically | ||
|
|
f7b9a818b5 |
docs: promote one-command quick start and refresh social card (#419)
README: - Add a one-command Quick Start above Key Features (single docker run) to show how fast setup is; rename the detailed section to Deployment and drop the duplicated one-liner. - List TinyWow among the alternatives. - Tighten the sponsor call to action. Branding: - Replace the social card's GDPR/HIPAA badges with Self-hosted, Privacy-sensitive, Compliance-friendly, Air-gap capable, Open source, matching the landing Hero. - Regenerate branding/social-preview.png and the synced landing/web OG images (counts refreshed to the live catalog). Claude-Session: https://claude.ai/code/session_01JQ8LmV8LPLi8yNTayHzSTQ |
||
|
|
2a36b3dfe0 |
fix(ai): update arm64 bundle sha256/size after protobuf<5 rebuild (#421)
Rebuilt the object-eraser-colorize, ocr, and transcription arm64-cpu bundles with the protobuf<5 pin (PR #417) and republished them to deepsafe/feature-bundles/v2.0.0. Update the manifest archive checksums, compressed sizes, and (previously 0) extracted sizes to match the new tarballs so install_feature.py's sha256 verification passes. All three rebuilt bundles bake protobuf 4.25.9; verified gzip-clean and that paddle 3.2.2 / onnxruntime coexist with protobuf 4.25.9 on aarch64. Claude-Session: https://claude.ai/code/session_01VtvE6K8iEr5jGFJJpHEaPA |
||
|
|
ea004a3308 |
feat(landing): swap Try Demo for Get Started Free CTA in top nav (#418)
Book a Demo moves into the outline nav slot (Try Demo is removed), and a new Get Started Free primary CTA links to the docs getting-started guide in a new tab. Applies to both the desktop bar and the mobile menu. Claude-Session: https://claude.ai/code/session_01STf4CGEkYKNwN26rMxVpn6 |
||
|
|
2c2fb65fca |
fix(ai): pin protobuf<5 on arm64 so mediapipe face landmarks work (#417)
* fix(ai): pin protobuf<5 on arm64 so mediapipe face landmarks work aarch64 has no mediapipe wheel above 0.10.18, and 0.10.18 calls MessageFactory.GetPrototype (removed in protobuf 5+). With protobuf unpinned, the paddle/onnxruntime deps pull protobuf 7.x into the shared AI venv and break mediapipe FaceLandmarker, so red-eye-removal fails on every input (blur-faces and smart-crop keep working via a prebuilt graph). Split mediapipe by platform and pin protobuf>=4.25.3,<5 for aarch64 only. x86_64 keeps mediapipe 0.10.35, which works with protobuf 7, so requirements-gpu.txt (amd64 only) stays unpinned. Also fixes a latent issue where mediapipe>=0.10.21 was unsatisfiable on aarch64. Verified live on the arm64 container: red-eye-removal completes on real jpg and heic faces; OCR (tesseract) and paddle import unaffected. Claude-Session: https://claude.ai/code/session_01VtvE6K8iEr5jGFJJpHEaPA * fix(ai): pin protobuf<5 in arm64 bundles lacking a mediapipe constraint Bundles are built from docker/feature-manifest.json, not requirements.txt, so this is the change that actually fixes the shipped arm64 bundles. On arm64, object-eraser-colorize (onnxruntime), ocr (paddle) and transcription (faster-whisper pulls onnxruntime) install a protobuf-dependent package with no mediapipe to cap protobuf, so they bake protobuf 7.x. All bundles share one /data/ai/venv at install time, so whichever of those installs last overwrites protobuf to 7.x and breaks mediapipe FaceLandmarker (red-eye-removal). Pin protobuf>=4.25.3,<5 in those three arm64 lists (appended last so it downgrades after the puller installs). The four mediapipe bundles already resolve <5. Dry-run on aarch64 confirmed paddle + protobuf 4.25.9 resolve with no conflict. Claude-Session: https://claude.ai/code/session_01VtvE6K8iEr5jGFJJpHEaPA * refactor(ai): keep protobuf fix in feature-manifest.json only requirements.txt is not consumed by the Docker image build (the base /opt/venv is installed from a hardcoded package list, and the ML libs ship via bundles), so the requirements changes had no effect on shipped artifacts and only tripped the dependency-review scanner on the protobuf range. Revert them; the operative arm64 bundle fix lives entirely in docker/feature-manifest.json. Claude-Session: https://claude.ai/code/session_01VtvE6K8iEr5jGFJJpHEaPA |
||
|
|
8b3f1e6884 |
fix: stamp SnapOtter as Producer on generated PDFs (#416)
Conversion engines wrote their own names into PDF metadata: LibreOffice, Ghostscript, pdfcpu, WeasyPrint, and PDFKit all stamped Producer/Creator on generated files. A new doc_scrub_meta docs-profile script (PyMuPDF) rewrites both fields to SnapOtter and drops the stale XMP copy; the worker applies it to the 25 PDF-generating tools before outputs reach object storage. Best effort by design: any failure keeps the original bytes and only logs a warning. Deliberately untouched: tools that edit the user's own PDF and preserve its metadata (qpdf edits, sign, flatten), encrypted outputs (copied through), and pdfa-convert, where a metadata rewrite risks PDF/A conformance. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 |
||
|
|
4d37092bbe |
ci: move path filtering to job level so required checks stay mergeable (#415)
Workflow-level paths-ignore never creates the check runs on docs-only PRs, which would deadlock required status checks (contexts stay "expected" forever). A new always-on changes job diffs against the base commit and every downstream job skips when only README, CONTRIBUTING, branding, apps/docs, apps/landing, or docs changed. Skipped jobs report a conclusion, which satisfies branch protection, so main can now require the full CI matrix without blocking docs-only changes. Fails open: unknown base (force push, dispatch, new branch) runs the full pipeline. Prerequisite for enabling required status checks on main. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 |
||
|
|
bf417a509e |
fix: first-run QA sweep of the single-container image (#413)
Fixes found by manually testing a fresh install end to end: - auth: the must-change-password gate returned 403 on public routes including /api/v1/health, so every fresh install showed a false "Reconnecting to server" banner on the forced password change screen. Public routes are now exempt (they need no session at all). Adds the gate's first direct tests. - multipart: @fastify/multipart's parts() iterator (9.4.0 and 10.0.0) ends on the request stream's "close", which on a reused keep-alive connection fires while an earlier part is still streaming to storage, silently dropping the parts behind it. The object eraser lost its mask file on every second POST per connection. Replaced with a busboy-driven iterator (lib/multipart-parts.ts) that ends on busboy's own "finish", installed for all routes via a preValidation hook; the tool-factory field-recovery workaround for the same bug is now unnecessary and removed. - eraser: the mask canvas backing store is natural resolution, but "absolute inset-0" does not stretch replaced elements, so the canvas rendered at intrinsic size and the brush ring, strokes, and exported mask were all misscaled on photos larger than the viewport. The canvas now gets an explicit CSS box at the fitted size. - compare slider: solid white divider with a dark halo so it stays visible over light images; still initialised at the painted region. - tool page: the AI bundle install prompt now centers in the content area instead of hugging the top. - api docs: disabled Scalar's cloud features (Ask AI, Generate MCP, Open API Client, dev toolbar), hid the "Powered by Scalar" footer link, and set the page title to "SnapOtter API Reference". The docs CSP blocks those cloud calls by design, so the buttons were dead UI. - docker: embedded Redis comes from packages.redis.io pinned to the 8.x major (was Debian's 7.0.15), matching the Compose stack and the documented claim. Build fails fast if the major ever drifts. - docs: DOCKERHUB.md quick start now leads with the one-command docker run (matching the README) with Compose as the production path; README says embedded Postgres 17 + Redis 8. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 |
||
|
|
3d4a84d068 |
feat(landing): warm up the contact CTAs (#412)
Give the contact calls-to-action a friendlier, more human voice. The nav "Contact" link and the FAQ "Contact Us" button become "Talk to a human", and the Enterprise pricing "Contact Sales" becomes "Let's talk". All links still point to /contact; only the labels change. Claude-Session: https://claude.ai/code/session_01MbQGV1k1mneFidgGyDnmDQ |
||
|
|
5500facc5d |
feat(landing): add animated shadergradient hero background (#411)
A self-hosted animated warm gradient now sits behind the hero: a captured WebGL mesh loop (webm + mp4 + webp poster, no framework or Three.js) in a top band that fades to cream before the tool search. The headline goes white, badges become dark glass, and the navbar is more opaque so nav, badges, headline, and subtitle all stay legible over the gradient. Reduced-motion and small screens fall back to the still poster. Also carries the hero copy already present in the working tree (new headline, badge labels, and subtitle), which is entangled with these edits. Claude-Session: https://claude.ai/code/session_01MbQGV1k1mneFidgGyDnmDQ |
||
|
|
b4375e558d |
fix: harden install queue/dispatcher lifecycle and repair review-sweep regressions (#395)
Fixes 15 defects found by a max-effort multi-agent review of the last 6 merged PRs (#388, #390, #391, #392, #393, #394), all adversarially verified before fixing. Install queue + dispatcher (the serious cluster): - features.ts: finalize the installer child exactly once. A failed spawn fires both "error" and "close", and the second event released the file lock and active slot that pump() had just handed to the next queued bundle, letting two pip processes write the same venv concurrently. Outcome recording now happens before pump() so the next bundle's first progress frame cannot race the previous install's bookkeeping. - feature-status.ts: keep failed-install errors in a per-bundle map instead of the single progress slot. With the queue auto-starting the next install, the slot was overwritten within seconds and a failed install vanished without ever surfacing to GET /features. - bridge.ts: scope child lifecycle per process (stopped-children set + request generation tags) instead of an instance-wide shuttingDown flag that the next spawn reset. A stale SIGTERMed child's late close event could record a phantom crash (5 of which permanently disable the dispatcher), null out the freshly spawned child, and reject the new child's pending requests. The request-timeout kill path still counts as a real crash. - install_feature.py: the pre-write disk re-check measured ai_dir's filesystem even when budgeting the cross-filesystem copy that lands on the venv's disk; now each budget is checked against the filesystem the bytes actually land on, so ENOSPC cannot strike mid-write and leave site-packages half overwritten. Behavior regressions: - embed-subtitles: preserve pre-existing subtitle tracks (0:s?) and MKV attachments (0:t?) that the -map 0:v:0/0:a? rewrite silently dropped; data streams stay unmapped on purpose (the actual MPEG remux fix). The new subtitle maps first so the language tag hits the right stream. - usage-survey-overlay: fail closed when the settings fetch fails; the fail-open path rendered the blocking survey against an unhealthy API and soft-locked admins, the lock-out class #392 fixed. - features-store: queued bundles poll instead of each holding an SSE connection (Install All could pin 7 EventSources and exhaust the browser's 6-per-origin HTTP/1.1 limit, hanging the whole app); listenToProgress closes any prior stream and stops any poll before subscribing; installAll skips bundles already installing or queued. Contracts, tests, i18n: - openapi.yaml: add "queued" to the features status enum and document downloadBytes/installedBytes (Schemathesis conformance). - feature-lifecycle e2e: queue transcription (~0.5 GB) instead of ocr (~6 GB) and give the test a budget that covers both install drains (the stacked waits exceeded the old 900s timeout). - docker-compose.qa.yml: parameterize the host port (QA_APP_PORT) so QA_PROJECT_NAME concurrent stacks can actually bind. - compare + watermark-image: restore per-input error attribution ("Invalid first/second image", "Invalid watermark image") lost in the shared-handler migration. - ai-features-section: the "{size} on disk" suffix now goes through i18n; key added to all 21 locales. - watermark-image + content-aware-resize: migrate to the shared inputHandlerFor("image") chain like compare/vectorize/compose, fixing drift in the inline copies (no SVG sanitize, no RAW extension hint, no AVIF probe). Verified: typecheck across 9 workspaces, Biome clean on all changed files, 584 targeted unit tests and 249 integration tests green (including real-ffmpeg embed-subtitles runs). One unit test updated to the new poll-while-queued contract with a single-EventSource assertion. Claude-Session: https://claude.ai/code/session_017mR1HiHaf3a1BmUtrHX4j3 |
||
|
|
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 |
||
|
|
4c32fee039 |
refactor: derive feedback-dialog's remaining option arrays from the shared source (#394)
Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp |
||
|
|
ca076f91fd |
fix: critical first-login soft-lock in usage survey overlay (#392)
* fix: prevent UsageSurveyOverlay from soft-locking the first-login password-change flow Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * fix: prevent double feedback submission when the settings write fails Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * refactor: consolidate feedback enums into packages/shared as a single source of truth Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * feat: add ARIA semantics, dismiss-button guard, and shared auth-route list to UsageSurveyOverlay Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * test: cover the submit-failure retry path and a persona-only minimal payload Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp |
||
|
|
bd1838e40b |
fix: repair docker validation QA tooling, dispatcher crash-accounting, and image-enhancement RAW hang (#391)
Found and fixed during a full local Docker build validation (amd64/arm64, all four fleet targets, AI bundle installs, QA harness) and the follow-up bug sweep requested afterward. None of the affected scripts run in CI, so these had been silently broken indefinitely. - docker/feature-manifest.json: pythonVersion was a flat "3.11", but the amd64 base (Ubuntu 24.04) ships Python 3.12 while arm64 (Debian bookworm) ships 3.11. Changed to a per-arch object matching the file's existing convention. - tests/qa/api-sweep.mts and verify-ai.mts: bare "@snapotter/shared" import can't resolve since tests/ is not a pnpm workspace member, making both silently unrunnable via their own documented command on any fresh checkout. Switched to a relative import. - tests/qa/generate-ledger.mts: wrote to docs/qa/ without creating the directory first; docs/ is gitignored except COMMUNITY_GUIDE.md, so a fresh checkout threw ENOENT. - Seven QA Playwright spec files (input-preview, settings, settings-extended, multifile, output-preview, pipeline-ui, smoke) had ~115 fixture() calls using directory names that don't exist. Resolved every call programmatically against the real fixture tree. - packages/ai/src/bridge.ts: AI dispatcher restart (happens on every bundle install) was falsely counted as a crash, risking permanent dispatcher disable after enough legitimate restarts within the crash window. Added a shuttingDown flag checked at all three recordCrash() call sites. - packages/image-engine/src/operations/auto-enhance.ts: image-enhancement hung 40+ seconds on large RAW photos (confirmed on a real 20.2MP file) in Sharp's .clahe() step, whose cost scales with total pixel count regardless of tile size. Added a 16-megapixel cap above which CLAHE is skipped; verified against the real file (40+s -> 2.0s) with no regression to other RAW formats or normal-sized images. Fixing this surfaced a second, smaller bug where the saturation step's CLAHE compensation boost was keyed off the raw toggle instead of whether CLAHE actually ran. - Two QA-harness robustness gaps closed per "fix everything, even the small bugs": the passport-photo/erase-object input-preview tests now skip cleanly with a clear reason on a container without their AI bundle installed, and docker-compose.qa.yml's hardcoded project/container name (the actual root cause of a mid-validation container swap between two concurrent sessions) is now parameterized via QA_PROJECT_NAME. Full validation report is local-only per repo convention. |
||
|
|
7e01d3637e |
fix(docker): repair copied AI venv paths (#390)
AI feature installs now keep copied Python venv metadata (bin/pip shebang, bin/activate, pyvenv.cfg) pointed at /data/ai/venv, so scripts no longer silently fall back to the baked, read-only /opt/venv after the venv is bootstrapped into /data. Fixes #127 (AI tools incompatible with PUID/PGID). The entrypoint repairs both fresh bootstraps and already-stamped runtime venvs (self-heals existing deployments on next restart, no reinstall needed), with regression coverage for literal path replacement and binary file safety. Independently reviewed and verified: traced chown/gosu ordering in entrypoint.sh to confirm no permission regression, reproduced the exact issue #127 scenario (custom PUID + manual venv activation) in a live container both before and after the fix, and ran the PR's own test suite locally (16/16 passing). Co-authored-by: SyntaxSawdust |
||
|
|
a0d1c70172 |
feat: add usage onboarding survey overlay (#388)
* feat: add usage-survey feedback types and gating function Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * feat: add onboarding usage-survey i18n strings to all locales Relabels three ambiguous feedback.usageTypes values (personal/team_internal/ business_workflow) and adds a new onboarding namespace (4 keys) across the reference locale and all 20 translations, so the tree compiles at every commit instead of only after both locale groups land. Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * feat: add UsageSurveyOverlay component * feat: mount UsageSurveyOverlay inside AuthGuard Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * fix: use text-start instead of text-left for RTL support in UsageSurveyOverlay * refactor: drop redundant usage-type field from the admin feedback dialog Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * feat: accept onboarding source and survey id in the feedback route Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * test: cover the onboarding source in the feedback route integration test Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * chore: remove orphaned usageTypeLabel i18n key * refactor: derive feedback source/survey_id enums from a single source of truth * perf: skip the settings fetch in UsageSurveyOverlay for non-admin users |
||
|
|
9052da27f3 |
fix: adopt sharp 0.35.2+ by centralizing the FormatEnum key type (#362)
Centralizes the sharp format key type into a single `SharpFormat` alias derived from `toFormat()`'s signature (exported from @snapotter/image-engine, imported by the API consumers), replacing the duplicated `keyof FormatEnum` definitions. Adds convert/compress format round-trip tests covering webp/avif/png/jpeg. Part of #325. |
||
|
|
852f6cecba | fix: show modality starting points in landing command center (#387) | ||
|
|
3b1d484b4d | feat: add landing tool command center (#386) | ||
|
|
c68297d5a4 |
feat: request a tool when home search finds nothing (#385)
Adds a prefilled 'Request a tool' affordance to the home search empty state and beneath weak results. Opens the in-app feedback dialog with a new search_miss source and a structured search_query when analytics is on; links to a prefilled GitHub Discussions (Ideas) post when off, so a request is never silently dropped. Reuses the existing feedback pipe, dialog, and analytics gate; no new storage. i18n across all 21 locales. |