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
* 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
* fix(landing): correct PDF tool count to 28 in alternatives copy
The pdf section has 28 tools (section.test.ts asserts bySection('pdf')=28). PR #363 corrected the docs breakdown but the alternatives pages still said 40 PDF tools (the document-modality count, not the pdf section) with 200 for the rest. Update to 28 PDF tools and 212 for the non-PDF remainder.
* chore: use "200+ tools" for the tool-count claim across public surfaces
Replaces the exact '240 tools' count (which drifts as tools are added) with the stable '200+ tools' on README, the Docker Hub overview, landing pages, the docs site (meta, homepage, search), the API self-description, the demo OG tag, llms.txt, package.json, the branding readme, and the en/nl app strings. Per-modality breakdown tables stay exact. Leaves the architecture doc's technical 'tool routes' figure, an internal vitest comment, and a QA report line unchanged. Updates the two tests that assert the docs strings.
Lands five integrated branches: pipeline templates (#355), analytics opt-out (#354), 83 conversion presets bringing the catalog to 240 tools (#356), self-hosted positioning (#353), and e2e modernization (#351).
Integration fixes: aligned stale web analytics tests with the opt-out/allow-list model, closed 3 CodeQL incomplete-sanitization alerts in the i18n generator, resolved settings/index/docs/format-matrix conflicts, and corrected tool counts to 240.
The nightly Schemathesis job failed with a schema-loading error:
'unacceptable character #x0080: control characters are not allowed'. The
served openapi.yaml contained 215 em dashes (U+2014) and box-drawing
section dividers (U+2500); Schemathesis's strict YAML parser mis-decodes
those multi-byte UTF-8 sequences as C1 control chars and refuses to load
the schema, so no fuzz checks ran. (PyYAML is lenient, which is why the
local yaml.safe_load check passed.)
Replace every non-ASCII char with ASCII '-'. This also clears an
em-dash style-rule violation. Add a docs.test.ts guard asserting the
served spec is ASCII-only, with a clear message, so this can't regress.
Pre-existing issue (the dashes predate this branch); surfaced while
verifying CI is green.
* fix(test): repair integration suite after analytics column/endpoint removal
#336 moved analytics to a build-time bake: migration 0005 dropped the
users.analytics_enabled and analytics_consent_* columns and removed the
PUT /api/v1/user/analytics endpoint. Two integration tests were left
referencing the old shape and went red on main (13 failures):
- migrate-from-sqlite.test.ts built 1.x SQLite fixtures whose users table
declared the analytics columns. The generic SELECT *-based importer then
tried to INSERT them into the 2.0 target, which no longer has those
columns, failing with Postgres 42703 and rolling back the whole import
(cascading to all 12 assertions). 1.x never had analytics columns, so the
fixtures are corrected to drop them. Also removed the now-dead analytics
entries from the importer's TS/BOOL conversion sets.
- analytics.test.ts asserted the removed PUT endpoint returns 404 but sent
the request unauthenticated, so the global auth preHandler answered 401
first. It now authenticates, reaching Fastify's not-found handler (404).
Also removed the stale /api/v1/user/analytics path from openapi.yaml.
Verified locally: full platform integration bucket 1029 passed / 0 failed;
monorepo typecheck clean.
* test(e2e): drop orphaned analytics-consent dismissal calls
#336 deleted the entire analytics consent system (consent page, consent
module, and PUT /api/v1/user/analytics), but six tests/e2e files still
PUT to that removed endpoint to 'dismiss analytics consent.' The calls
were silent no-ops (Playwright request.put / fetch don't throw on 4xx),
so they passed while hitting a dead route.
There is no consent prompt to dismiss anymore, so remove the calls:
- auth.setup.ts / qa-auth.setup.ts: keep the waitForFunction that syncs on
login completion, drop the now-unused token capture, the dead PUT, and
the stale 'consent guard' comments.
- rbac / rbac-full / gui-settings-rbac / gui-settings-expanded specs: the
re-login blocks existed solely to obtain a token for the PUT (reLoginData
was used nowhere else and the block was the tail of each helper), so
remove the whole block. The meaningful create-user/login/change-password
work is untouched.
Verified: no /api/v1/user/analytics refs remain in tests/e2e; biome clean
(no unused vars).
* docs: rebrand from image-only to multi-modality across docs and metadata
SnapOtter expanded from image-only to 157 tools across 5 modalities
(image, video, audio, document/PDF, data). Update all product-level
copy, metadata, and i18n that still framed it as an image-only tool.
- README, package.json, root llms.txt: multi-modality framing, 157 tools
- OpenAPI info + tags, generated /llms.txt tagline (docs.ts)
- VitePress docs site: hero, getting-started, architecture, security,
deployment, configuration, developer, supported-formats
- i18n: 10 product keys across all 21 locales (hero, app description,
privacy notes, AI features, progress messages, getting-started)
- web/demo/landing meta + privacy copy, COMMUNITY_GUIDE, .env.example
Stale tool counts (53/50+/52/70+/35) corrected to 157 throughout.
Database/container deployment claims left unchanged (out of scope).
* docs: fix stale post-rebrand test assertions and README language list
- tests/e2e-docs/homepage.spec.ts: assert the current docs homepage (file toolkit, 157 tools, 5 modalities) instead of the old image-only strings
- tests/unit/api/docs-route.test.ts: sync the reproduced llms.txt tagline with docs.ts
- README.md: 21 languages with the correct list (add Swedish and Chinese Traditional, drop Czech which is not supported)
* docs: correct 2.0 architecture references (Postgres 17 + Redis 8, 3-container stack)
The docs and metadata still described the 1.x stack (SQLite, single container, p-queue). Update them to the current 2.0 reality.
- README: replace the broken single-container `docker run` quick-start with the real Docker Compose stack (app + Postgres 17 + Redis 8); fix the "no Redis, no Postgres" feature bullet
- package.json: description no longer claims a single container
- apps/docs: rewrite database.md for Postgres; configuration.md DB_PATH -> DATABASE_URL + REDIS_URL; architecture.md SQLite/p-queue/better-sqlite3 -> Postgres/BullMQ/pg and add media-engine + doc-engine; developer/security/deployment/docker-tags/getting-started/contributing compose examples now include postgres + redis; index.md + api/ai.md AI count 16 -> 19
- SECURITY.md: Drizzle (SQLite) -> (PostgreSQL)
- landing: enterprise/FeatureHighlights single-container wording; TrustSignals/ToolGrid 150+ -> 157 (dynamic); Pricing/FAQ 15 -> 19 AI tools
* docs(api): document all video, audio, document, and data tool endpoints in OpenAPI
The spec covered only image tools; the Scalar UI and the generated /llms.txt and /llms-full.txt inherited that gap. Add the 104 missing tool endpoints so the API docs match the code.
- Video: 29 endpoints (most long/async; auto-subtitles is AI)
- Audio: 17 (transcribe-audio is AI)
- Document/PDF: 36 (ocr-pdf is AI; conversions are long/async)
- Data: 10
- Image: 12 newer tools (background-replace, blur-background AI; histogram/lqip-placeholder/sprite-sheet custom responses; barcode-generate uses a JSON body)
Each schema is derived from the tool's Zod validator and executionHint (fast -> 200, long -> 202+SSE, AI adds 501 FeatureNotInstalledError, multi-file inputs as arrays), referencing the existing shared schemas. Tool path entries: 64 -> 168. Spec parses as valid YAML with no duplicate paths and only known $refs.
- Create 53 per-tool VitePress documentation pages with accurate
parameters from Zod schemas, example requests, and response formats
- Add root llms.txt for LLM-friendly repo browsing
- Fix OpenAPI spec: add auth and 422 error schemas to
edit-metadata/inspect and strip-metadata/inspect sub-routes
- Fix tool count inconsistency (52 -> 53) across landing site,
e2e tests, and local docs
- Rename color-adjustments.ts to adjust-colors.ts to match tool ID
- Update VitePress sidebar with all 8 tool categories and top nav
- Bump all workspace package versions to 1.17.0
- Update APP_VERSION constant and OpenAPI spec
- Update AI tool count from 15 to 16 across docs and i18n
- Update tool table with AI Canvas Expand, Meme Generator, Beautify
- Add image editor, OIDC, and 20 languages to README features
- Add release notes for v1.17.0
- Add JSON-LD structured data and SEO improvements to landing/docs
ai-canvas-expand was added to constants.ts and route files but never
added to the landing page bento grid, causing all hardcoded counts
to remain at 51. This updates all references across source, docs,
i18n, and tests to reflect the correct count of 52 tools.
Replace fragile Unicode-range regex with positive ASCII check
(/^[\x20-\x7E]+$/) that also catches emoji and supplementary plane
characters. Update OpenAPI spec to document percent-encoding.
- Add meme-generator tool and meme-templates API to OpenAPI spec, VitePress
docs, and README
- Remove 4 phantom OpenAPI entries (brightness-contrast, saturation,
color-channels, color-effects) that were consolidated into adjust-colors
- Bump OpenAPI version to 1.16.0, tool count to 51
- llms.txt and llms-full.txt auto-update from OpenAPI at runtime
Add beautify tool endpoint to OpenAPI spec with full parameter
documentation. Add tool to REST API docs Layout & Composition table.
Update tool count from 49 to 50 across README, docs homepage,
architecture docs, VitePress config, OpenAPI spec, and LLM docs
generator.
Users can no longer customize the app name or logo. The branding API
endpoints, permission, frontend UI, env vars (APP_NAME, MAX_LOGO_SIZE_KB),
and all related tests are removed. Includes a migration to clean up
branding data from existing databases.
- Add Cloudflare Pages deployment for landing page (snapotter.com) and
docs (docs.snapotter.com)
- Create deploy-landing.yml and update deploy-docs.yml workflows
- Update CI to ignore apps/landing/** paths
- Fix logo transparency (remove white background) across all apps
- Recreate social-preview.png with SnapOtter branding
- Update all docs URLs from GitHub Pages to docs.snapotter.com
- Update VitePress config: light theme default, fix llms.txt paths
- Add .vitepress/cache/ and .env.* to gitignore
Add 12 previously undocumented routes to the OpenAPI 3.1 specification:
content-aware-resize, edit-metadata (+ inspect), stitch, pdf-to-image
(+ info, preview), gif-tools/info, remove-background/effects, preview,
pipeline/tools, and pipeline/batch. Fix license from MIT to AGPL-3.0,
correct DELETE /files response from 204 to 200 with body, and update
VitePress API docs (rest.md tool table, ai.md model parameters). Also
register the sharpen operation in the image-engine OPERATION_MAP so it
can be used as a standalone pipeline step.
Co-authored-by: stirling-image <stirling-image@users.noreply.github.com>
Replace the confusing 2-mode smart crop with a clear 3-mode system:
- Subject Focus: Sharp attention/entropy saliency crop with social media presets
- Face Focus: MediaPipe face detection with headshot framing presets
- Auto Trim: Border removal with optional pad-to-square
Adds detectFaces() to AI package, face preset constants, backward
compatibility for old mode names, and comprehensive integration tests.
Update all references across docs, workflows, UI components, and config
to point to the new GitHub org (stirling-image/stirling-image) and Docker
Hub account (stirlingimage/stirling-image) ahead of repo transfer.