New tool for joining images horizontally or vertically,
distinct from the grid-based collage tool. Preserves aspect
ratios with fit/original resize modes, optional gap, and
multi-format output.
Users running lite mode had no way to tell why AI tools were greyed out.
Now the public health endpoint reports the variant, and a visible banner
appears in the tool panel when running in lite mode.
Prevent silent data corruption when the API is called directly
(bypassing UI guards). Binary/complex EXIF fields like MakerNote
are now filtered from fieldsToRemove in the image-engine operation.
Move sanitizeValue, parseExif, parseGps, parseXmp into the shared
image-engine package so both strip-metadata and edit-metadata can
reuse them. Includes 13 unit tests covering all four functions.
Design spec for new edit-metadata tool (issue #15). Covers common EXIF
field editing, GPS clearing, granular per-field stripping, and shared
metadata infrastructure extracted from strip-metadata.
Clickable localhost:1349 links are misleading on the GitHub Pages site
since users may not have Stirling Image running locally. Use plain path
references instead so it is clear these live on their own instance.
The links used /Stirling-Image/ (wrong casing) which VitePress then
prefixed with the base /stirling-image/, producing a double-prefixed
404 path. Remove the manual base so VitePress prepends it automatically.
Keep the main docker run command front and center. Lite and CUDA
variants are in a collapsible details block so the quick start
section stays scannable.
onnxruntime-gpu reports CUDAExecutionProvider as "available" just
because the library was compiled with CUDA support, even on machines
with no GPU. This made gpu_available() return True incorrectly,
causing upscale.py to try torch.device("cuda") and fall back to
Lanczos instead of running Real-ESRGAN on CPU.
torch.cuda.is_available() actually probes the hardware. Use it as
the single source of truth for GPU detection.
Verified: CUDA image on Apple Silicon (no GPU) now correctly reports
gpu: false and all AI tools run on CPU without crashes.
The STIRLING_GPU=true env var was baked into the :cuda Dockerfile,
which made gpu_available() return True without checking actual
hardware. On machines without a GPU, this would crash upscale.py
(torch.device("cuda") fails) and ocr.py (PaddleOCR use_gpu=True).
Fix: the env var can only disable GPU (set to false/0), never
force-enable it. Hardware detection always runs. Removed the
baked env var from the Dockerfile since it adds no value now.
- README: add CUDA docker run example alongside full and lite
- Getting started: add GPU acceleration tip with speedup numbers
- Deployment: add CUDA row to variants table
- Docker tags: expand benchmarks with warm + cold start tables
- Add :cuda tag to Docker Tags docs with setup, benchmarks, compose example
- Add GPU acceleration tip to AI engine docs
- Include benchmark table from RTX 4070 testing
Add a :cuda Docker image tag that auto-detects NVIDIA GPU at runtime
and falls back gracefully to CPU. Same pattern as Immich.
- New gpu.py shared utility for cached CUDA detection
- Background removal (rembg): pass CUDAExecutionProvider to ONNX Runtime
- Upscaling (Real-ESRGAN): use CUDA device + FP16 when GPU available
- OCR (PaddleOCR): enable use_gpu when CUDA detected
- Dispatcher reports GPU status at startup via readiness signal
- Admin health endpoint exposes GPU availability
- Dockerfile uses ARG GPU=false with conditional NVIDIA CUDA base image
- docker-compose.gpu.yml override for GPU users
- CI/CD workflows build and publish :cuda tag (amd64 only)
Three tags: :latest (CPU), :lite (no AI), :cuda (GPU with CPU fallback)
Centralize duplicated getToken() + Bearer header logic into a single
formatHeaders() helper in lib/api.ts. When no token exists, the
Authorization header is omitted entirely instead of sending an empty
Bearer token, which breaks forward-auth proxies like Authelia behind
Caddy.
Changes:
- Add formatHeaders() with try-catch around localStorage access
- Replace 20+ duplicated getToken() definitions across tool components
- Migrate all call sites including file-details, settings, change-password
- Update tests to verify header omission on empty token
Based on the fix proposed by @jules2689 in #6, with improvements:
file placement (lib/api.ts vs components), localStorage error handling,
simplified truthiness check, and complete call-site coverage.
Co-Authored-By: Julian Nadeau <julian@jnadeau.ca>
Both llms.txt and llms-full.txt incorrectly said MIT. Also added lite
variant mention to the description and a warning callout on the AI
engine docs page noting AI tools are unavailable in the lite image.
README Quick Start now shows both :latest and :lite commands.
Getting Started adds a tip callout about the lite image.
Deployment page lists both variants with a comparison table and
updates the CI/CD description to mention both are built.
Developer guide adds the lite build command.
Linux libheif packages provide heif-convert instead of heif-dec (which
is macOS-only). The decoder now tries heif-convert first, then falls
back to heif-dec. Both accept the same argument syntax.
Ubuntu 24.04 uses plugin-based libheif codecs. Added libheif-plugin-x265
(HEVC encoder) and libheif-plugin-libde265 (HEVC decoder) to the CI test
job. Debian bookworm (Docker) bundles these in libheif1 directly.
Adds a new mode that trims uniform-color borders around the subject,
like GIMP's "Crop to Content." Includes configurable tolerance threshold
and optional pad-to-square with target size for e-commerce workflows.
The original attention-based crop is preserved as "Focus Crop" mode.
Closes#7
New users on dark-mode systems were seeing dark theme on first visit.
The default is now explicitly light, matching the API's DEFAULT_THEME.
Users can still switch to dark or system in settings.
- Health endpoint returns "healthy" instead of "ok" for consistency
- MAX_USERS now configurable via env var (default 5)
- People API returns team names instead of UUIDs in register/list
- PUT user update accepts team names (name-first lookup, fallback to ID)
- Login rate limit follows global rate limit when RATE_LIMIT_PER_MIN > 1000
- Strip-metadata preserves original format encoding instead of always PNG
- Fix e2e tests: rotate/crop/border button selectors match actual UI
- Fix e2e tests: create Engineering/Design teams in people test setup
- Fix e2e tests: people UI uses select for team field, not text input
- Update visual regression baseline for tablet home page
Without the fallback, every Copy button is dead on HTTP. The
execCommand approach is deprecated but works in all current browsers
and does not require a secure context.
Addresses issues #4 and #5 - crypto.randomUUID() and
navigator.clipboard.writeText() fail over plain HTTP on
non-localhost addresses, breaking all tool operations.
Worker thread initialization imports the tool registry which reads SQLite.
Under Docker volume filesystems, this can deadlock silently on SQLITE_BUSY,
causing APPLY to spin at 0% forever. Sharp operations complete in milliseconds
and don't need worker offloading. Added 30s AbortSignal timeout as defense
in depth for future re-enablement.
Adds a gosu-based entrypoint that starts as root, fixes ownership of
/data and /tmp/workspace for the stirling user, then drops privileges.
This fixes "SQLITE database not found" errors when users bind-mount
host directories.
Remove socialLinks config and three-dots menu. Add appearance toggle
and combined GitHub + Star button directly in the navbar via layout
slot. The button shows the Octocat icon, "Star" label, and live
stargazer count.
Replace MIT with AGPLv3 + commercial dual-license. Add copyright header
and dual-license notice to LICENSE, add CLA to CONTRIBUTING.md, update
README license section, and update all package.json license fields.
SVG files were rejected by the convert endpoint because
validateImageBuffer only recognized raster magic bytes. This adds
text-based SVG detection, sanitization in the tool factory, and
proper Sharp density handling so SVG-to-raster conversion works
through the standard convert route.
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.
Add a privacy policy page accessible at /privacy (public, no auth required).
Relax Content-Security-Policy for /api/docs route to allow Scalar's inline
script initialization, fixing blank docs page in production.
Remove DB probe from public health endpoint - it only needs to confirm
the process is alive. Add test for non-admin user getting 403 on admin
health endpoint.
Public GET /api/v1/health now returns only status and version.
Full diagnostics (uptime, storage, database, queue) moved to
GET /api/v1/admin/health which requires admin authentication.
Validation now runs on all entries before any database writes.
Previously, clean entries could be written before a later malicious
entry triggered a 400 response.
PUT /api/v1/settings now returns 400 if any key or value contains HTML
tags. Settings are configuration values - there is no legitimate use
case for HTML in them.
The processed image in the BeforeAfterSlider was layered on top of the
original with a semi-transparent background. When the processed result
had transparency (e.g. after remove-background), the original image
showed through, making it look like the background was not removed.
Added an opaque checkerboard background behind the processed image so
transparent areas are clearly visible instead of showing the original.
Two issues caused intermittent step addition failures in the automation
pipeline:
1. RemoveBgControls had onChange in its useEffect deps. Since onChange is
a new function reference on every parent render, this created an
infinite re-render loop (effect -> setState -> render -> effect).
Fixed by using the onChangeRef pattern matching other settings
components.
2. All step mutation callbacks read from stepsRef.current and passed
values to setSteps. Concurrent callbacks (e.g. addStep + a settings
effect) would overwrite each other. Fixed by switching to functional
state updates (setSteps(prev => ...)) and removing stepsRef.
Also rewrites automate e2e tests to use manual step addition instead of
referencing templates that no longer exist in the UI.
Child component effects (Controls onChange) fire before the parent's
useEffect that synced stepsRef. This caused updateStepSettings to read
stepsRef.current as [] and wipe out newly added steps.
Fix: assign stepsRef.current = steps directly during render so the
ref is always current before any child effects execute.
All step mutation callbacks (addStep, removeStep, moveStep,
updateStepSettings) captured `steps` in their useCallback closures.
When React batched state updates, rapid interactions could use a stale
steps array, causing clicks to silently fail.
Fix: use a stepsRef that always holds the latest value. Callbacks read
from stepsRef.current instead of the captured closure variable, and no
longer need `steps` in their dependency arrays.
When a tool was selected via search, the search text persisted after
the picker closed. Reopening the picker showed a filtered list instead
of all tools, making it look like clicking tools without searching
didn't work.
The settings panel used conditional rendering ({isExpanded && ...})
which unmounted the Controls component on collapse, losing all state.
Switch to CSS hidden class so the component stays mounted and settings
persist when the panel is collapsed and re-expanded.
Extract a *Controls subcomponent from all 16 pipeline-compatible tool
settings components. Each Controls component holds the UI state and
settings controls, accepts an onChange callback, and uses useRef to
prevent infinite re-render loops. The standalone *Settings components
become thin wrappers that add useToolProcessor, useFileStore, and
action buttons.
pipeline-step-settings.tsx is rewritten from ~675 lines to ~55 lines:
the entire TOOL_FIELDS declarative map and generic renderer are deleted
and replaced with direct imports of the Controls components. Pipeline
steps now render the exact same UI as standalone tool pages.
Special cases:
- CropControls: numeric inputs for pipeline (standalone uses canvas)
- RotateControls: resetSignal prop for post-processing reset
- ColorControls: accepts toolId for tab selection
- StripMetadataControls: checkboxes only (no file inspection)
- RemoveBgControls: already extracted, unchanged
Extract the settings controls (subject type, quality, background color)
from RemoveBgSettings into a shared RemoveBgControls component that
accepts settings + onChange props. Both the standalone tool page and the
pipeline step configurator now render the same component, so the UI is
identical and changes only need to be made in one place.
The pipeline step settings had an empty array for remove-background,
showing "No configurable settings" even though the tool supports
model selection and background color. Add AI model selector (u2net,
birefnet-general-lite, birefnet-general, birefnet-portrait, bria-rmbg)
and background color picker matching the standalone tool's options.
Register remove-background, upscale, and blur-faces in the pipeline
tool registry via registerToolProcessFn(). These tools keep their
custom HTTP routes (with progress callbacks) for direct use, but now
also provide a simple process function for pipeline/batch execution.
Add a search bar to the pipeline tool picker so users can quickly
find tools by name or description. Uses the existing SearchBar
component and the same filtering pattern as the main tool panel.
Update tests to reflect that these 3 AI tools are now pipeline-
compatible (moved from excluded to included assertions).
Add 8 tests that would have caught the pipeline bug where custom-route
tools (remove-background, upscale, ocr, etc.) were shown in the
pipeline picker but failed silently when executed.
New tests:
- GET /api/v1/pipeline/tools returns factory-registered tool IDs
- Verify resize, crop, convert, compress, rotate are included
- Verify remove-background, upscale, ocr, blur-faces, erase-object,
info, collage, compare are excluded
- Pipeline execution returns 400 for each custom-route tool
The pipeline tool picker was showing all tools, but only tools
registered via createToolRoute() support pipeline execution. Tools
with custom routes (remove-background, upscale, ocr, etc.) would
silently fail with "Tool not found" and the empty catch block hid
the error from users.
Add GET /api/v1/pipeline/tools endpoint that returns the IDs of
pipeline-compatible tools. The frontend fetches this list and filters
the tool picker accordingly. Also surface pipeline execution errors
in the UI instead of swallowing them.
Safari, Chrome, and Firefox only offer to save passwords when they see
a real form submission with page navigation, not fetch() + redirect.
After the change-password API call succeeds, dynamically create a form
with the username and new password (autocomplete=username + new-password),
POST it to "/" causing a real navigation. The browser detects the form
submission with credential fields and prompts to save.
Also make the username field visible (read-only) on the change-password
page since Safari ignores hidden inputs for password detection, and add
autocomplete attributes to the login page fields.
Add a "Generate strong password" button that creates a random 16-char
password meeting all requirements (uppercase, lowercase, digit).
Generated passwords are shown in plain text so users can copy them.
Add autocomplete attributes (current-password, new-password, username)
so browsers prompt to save the new credentials after submission.
The backend sets mustChangePassword=true for all new accounts and
blocks API calls until the password is changed. The frontend was not
handling this flag - it logged the user in and redirected to the
dashboard where every API call silently failed with 403.
Add a /change-password page that is shown when mustChangePassword is
true. The login page now redirects there instead of home, and the
AuthGuard intercepts any direct navigation to force the change first.