Fixes the broken top navbar (logo/Features collision, wrapping CTAs). Removes the Developers link (kept in footer), adds shrink-0/whitespace-nowrap, and shows the horizontal nav at min-[1120px] with a hamburger below. Verified visually at 1024/1120/1152/1280px; 62/62 e2e, build green. Admin merge: landing-src-only, required matrix contexts path-skip and never report.
Follow-up to #469. Homepage/enterprise/FAQ JSON-LD and meta now say 'file-processing infrastructure' (not 'suite'), and the visible homepage counts (ToolGrid subtitle, Pricing bullet) plus SEO meta say '200+'. Removed the orphaned toolCount/TOOLS references. Leaves the /tools catalog and ToolGrid filter-tab functional counts exact. Admin merge: landing-src-only PR, required matrix contexts path-skip and never report.
Leads public copy with self-hosted file-processing infrastructure and demotes tool count to a proof point across README, docs, llms.txt, DockerHub, and the landing site. Adds a /self-hosted hub plus 7 job-intent SEO pages with a build-time validator, deepens the flagship /alternatives pages, adds a remove.bg page, three shared components, and landing e2e coverage.
Seed eight sample files across modalities in the demo Files library and mock the list (search + pagination), details with version history, bulk delete, and thumbnails. Thumbnails are generated colour-coded SVG tiles served through the interceptable fetch path, so the grid always renders with no real file bytes. Uploads/processing stay disabled.
The demo signs in as an admin on load (no login/change-password screen; /login and /change-password bounce to the dashboard), authEnabled stays true so the People/Teams/Roles/Security settings tabs remain available, the banner notes the admin data is sample data, and the mobile editor icon is an inline SVG so it always renders.
Rewrite the demo mock API around a seeded in-memory dataset with correct response shapes so the People/Teams/Roles/Audit/Usage/API-keys tabs stop crashing (the /auth/users vs /v1/users mismatch caused users.filter() on undefined) and show realistic sample data. In-memory CRUD makes the settings buttons work. Copy edit-image.png into the demo so the mobile editor icon renders. Adds unit shape guards and an e2e admin-settings walkthrough.
Add an OIDC / SSO login line to the Open Source tier and narrow the Enterprise bullet to SAML SSO + SSO enforcement. OIDC login ships in the free open-source build (env-gated, no license); only SAML and SSO enforcement are licensed.
Deep otter-brown Web 1.0 style bar pinned above the fixed navbar (offset via --lb-h), with a localStorage-remembered dismissal and a render-blocking check so returning visitors see no flash. Temporary; remove once 2.0 is old news.
Uninstalling a bundle only deletes its downloaded model weights, never the
shared venv's site-packages, so self-hosters who already hit an AI bundle
conflict (e.g. the scipy ABI strand) have no clean self-service path via
uninstall+reinstall: reinstalling just overlays corrected files on top of
stale ones. Adds POST /api/v1/admin/features/reset, which wipes
/data/ai/{venv,models,pip-cache}, resets installed.json, and reseeds a real
working venv from the image's baked /opt/venv (extracted docker/reseed-ai-venv.sh,
now shared with entrypoint.sh's existing base-venv-upgrade bootstrap instead
of duplicating that logic) -- leaving an empty venv directory here would
make the very next install fail with "spawn .../python3 ENOENT", caught by
testing this live rather than assuming it. Ships with a matching Settings UI
section (inline confirm, same pattern as per-bundle uninstall) and strings
across all 21 locales.
Verified against a real snapotter/snapotter:1.17.2 image migrated to 2.0.0,
with real multi-GB bundles installed (background-removal + OCR): confirmed
the migrated instance's inherited python3.11 venv (2.0.0 itself uses 3.12)
still imports the fixed scipy/numpy/paddleocr correctly, then reset + real
reinstall + actual tool execution (remove-background, verified output image)
all worked end-to-end.
* fix(api): prevent a crash when an over-limit upload stream has no consumer yet
busboy's "limit" handler destroyed the file stream with an error but never
attached its own error listener, relying entirely on whatever consumes
part.file downstream to do so. On a fast enough connection (or a fully
buffered body, e.g. Fastify inject()), busboy can process enough bytes to
hit the size limit before the route handler's receiveUpload() call has
attached its own stream listener, leaving the resulting "error" event with
zero listeners -- which crashes the whole process by default in Node.
Surfaced by tonight's FULL_MATRIX+FUZZ integration run (880 uncaught
exceptions, all the same root cause). Reproduces deterministically in
isolation; unrelated to this release's actual code delta (file untouched
since PR #413, well before the baseline QA pass).
Fix: attach a baseline no-op error listener the moment the stream is
created, guaranteeing at least one listener always exists. EventEmitter
delivers "error" to every registered listener, so the real consumer's own
error handling is unaffected.
* fix(ai-bundles): rebuild upscale-enhance and photo-restoration to reconcile scipy ABI
upscale-enhance and photo-restoration both depend on codeformer-pip, whose
transitive closure (basicsr -> realesrgan -> gfpgan) pulls in an unpinned
scipy. Both bundles were last built ~June 18-19, before PR #437 added the
manifest's `constraints` array (numpy==1.26.4, scipy==1.12.0, etc.) to pin
exactly this kind of dependency during bundle builds. Only the ocr bundle
was rebuilt after that fix landed.
install_feature.py has no pip install step -- it's a raw tarfile extraction
with no cross-bundle conflict resolution, so installing OCR alongside either
stale bundle left three incompatible scipy versions' files mixed in the same
site-packages directory (a compiled _rotation.*.so from one release next to
Python files expecting a different release's API), breaking the `upscale`
tool and OCR's higher-quality tiers with an ImportError.
Rebuilt both bundles for amd64-gpu and arm64-cpu from the current manifest,
verified scipy/scikit-learn/scikit-image/pandas all resolve to the pinned
versions in the tarballs themselves, then verified end-to-end on real
hardware (Mac arm64 CPU and ubuntu_gpu .248 RTX 4070): installing all
affected bundles together now yields exactly one version of each constrained
package, `upscale` produces correct output, and OCR's balanced/best tiers
correctly use PaddleOCR-GPU instead of erroring out.
Published the rebuilt tarballs to the public deepsafe/feature-bundles
HuggingFace repo and updated this manifest's sha256/sizes to match.
Also adds verify-bundle-compatibility.sh: verify-bundle.sh checks each
bundle in isolation (a fresh venv per bundle), which is exactly why this
shipped twice -- nothing ever checked that bundles built at different times
agree once layered into the one shared venv real installs use. The new
script installs every bundle for an arch into one venv and asserts each
constrained package has exactly one, correct version.
Known follow-up (not fixed here, needs separate discussion): uninstalling a
bundle only removes its downloaded model weights, never the site-packages
it added, so existing installations that already hit this bug have no clean
self-service fix via uninstall+reinstall -- they need a full AI-venv wipe.
* fix(docker): bake a real rate limit default for the all-in-one one-liner
The documented single-container `docker run` install had RATE_LIMIT_PER_MIN=0
(effectively unlimited, ~50k/min) baked in, since only docker-compose.yml
carried a hardened override. A self-hoster following the one-liner path got
no meaningful throttling anywhere, including auth-adjacent routes with no
dedicated per-route limit. Bakes a generous-but-real 1000/min default into
the Dockerfile, raises both compose files' fallback to match so the two
documented install paths converge on the same posture, and updates the Zod
schema default plus docs that quoted the old value.
* fix(api): boot log undercounted tool routes by the conversion-preset total
The "Tool routes: N active" line logged before registerConversionPresets(app)
ran, so it only ever reported the base 158 tools, 83 short of the real
241-tool total. Presets have to register after the base loop (they delegate
to each base tool's own processV2), so the fix moves the log line to after
that call and has registerConversionPresets return its count instead of
reordering the dependency.
* fix(ai): forward {info}/{warning} stderr JSON instead of dropping it
The dispatcher stderr parser only recognized {ready} and {progress,stage}
shaped JSON lines; anything else that parsed as valid JSON (like ocr.py's
GPU-to-tesseract downgrade notice, an {"info": ...} line) matched neither
branch and fell through silently, never reaching docker logs. Adds explicit
{info}/{warning} handling that forwards to console.log/console.warn, same as
the existing [prefix]-tagged non-JSON path.
* fix(api): fall back to a lower OCR tier when PaddleOCR itself is unusable
ocr.ts already retries lower quality tiers on a crashed dispatcher, but the
condition only matched crash-style messages (segfault, exited unexpectedly).
ocr.py's own ImportError/exception handlers already produce messages telling
the caller to use a lower tier (e.g. on the scipy ABI conflict class of bug),
but nothing ever acted on them, so a broken PaddleOCR hard-failed with 422
instead of degrading to Tesseract like ocr-pdf effectively does. Broadens the
retry condition to also catch PaddleOCR-engine-unusable messages.
Note: ocr-pdf's tesseract-only behavior turned out to be an unrelated,
pre-existing, deliberate design choice (PaddleOCR segfaults on rasterized PDF
pages on arm64), not a graceful-fallback mechanism to copy -- the two tools
weren't actually solving the same problem, so this fixes ocr.ts's own gap
rather than trying to mirror ocr-pdf.
Updates the homepage.spec.ts hero assertions (page title, headline, subtitle, trust badges) and the navbar contact link label to match the current landing copy. Verified 18/18 homepage e2e tests pass against the live landing.
Adds a click-to-copy Docker quickstart command and a small live-demo button to the landing hero, between the category cards and the stats. Removes the divider above the stats and tightens hero spacing so the stats stay above the fold. New HeroQuickstart.astro is a scoped copy-to-clipboard island (clipboard + execCommand fallback, aria-live, reduced-motion).
* docs: add 1.x-to-2.0 migration guide and upgrade notice
Adds MIGRATING.md with backup and upgrade steps, plus a short
"coming from 1.x?" callout in README and the docs upgrade guide
pointing existing users at it.
* fix: replace stale image-only and pre-rename data copy across product
SnapOtter grew from an image-only tool into a 5-modality suite
(Image, Video, Audio, PDF, Files), but copy in several places never
caught up. Fixes:
- dropzone.defaultFormats (i18n): every non-English locale still had
the pure pre-2.0 image-only format list; English omitted Files
entirely. Corrected across all 21 locales.
- settings.about.appDescription (i18n): "document, and data" workflow
copy updated to "PDF, and file" across all 21 locales.
- constants.ts: Files category's raw name was still "Data Files".
- Landing hero subtitle, JSON-LD schema, llms.txt, and 7 spots in the
competitor-comparison pages.
- Docs: VitePress config, supported-formats, deployment, and an
architecture.md modality-naming nit.
- OpenAPI description, root package.json description/keyword, and a
GitHub issue template dropdown option.
DOCKERHUB.md's separate "v1.x, image tools only" pre-release notice
is left untouched since 2.0 hasn't published to Docker Hub yet.
* test: update dropzone format-hint assertion to match corrected copy
The expected string still had the stale image-only/duplicated
PDF-Documents text from before the dropzone.defaultFormats fix.
An audit comparing every locale's leaf values against en.ts found
structural key-parity was already perfect (0 missing/extra keys), but
~2,356 leaf values across the 20 non-English locales were still
byte-identical to the English source, meaning they'd shipped untranslated.
The largest single cause: the feedback widget (PR #428) shipped with
English placeholder text in every locale except Italian, which had a
separate community translation (#425/#426).
Translates the 1,289 strings that were genuinely missing translations,
locale by locale, matching each file's own established register,
terminology, and loanword conventions (verified against already-translated
sibling strings rather than assumed). Leaves the remaining ~1,067 flagged
values untouched where they're legitimately identical to English: brand
names, format/protocol acronyms, literal URLs, hex colors, and terms this
project already treats as loanwords in that language.
Verified against current main: pnpm typecheck 0 errors (9/9 workspaces),
i18n-parity/i18n-locale/tool-i18n/template-i18n 45/45 passing (23/23
cross-locale parity), biome check clean.
Lands albanobattistella's Italian translation update from #438, with
one duplicate key corrected so it passes CI.
Their PR retranslated several terms in it.ts, but a stray edit left
watermark-video with two `submit` keys (the second, "Applica
spaziatura interna", was unrelated leftover text). Removed the
duplicate so `submit` stays "Applica filigrana", consistent with the
other watermark strings in the same diff.
Original translation by albanobattistella in #438; duplicate-key fix
by the maintainer.
Claude-Session: https://claude.ai/code/session_018tNg52r7b3RMEeybv5LHCX
Co-authored-by: albanobattistella <34811668+albanobattistella@users.noreply.github.com>
Dependabot proposed nvidia/cuda 12.6.3 to 12.9.2 (#441), a minor bump the existing semver-major ignore did not catch. The 12.6 base is deliberate: the AI bundle wheels are cu126, libcublas-12-6 matches, and a 12.9 base requires driver R575+ so the container fails to start on common 570.x production drivers (the regression PR #334 fixed). Ignore nvidia/cuda >= 12.7 so 12.6.x patch bumps still flow while 12.7+ is held back.
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
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
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
* 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
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
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
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
* 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
* 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