Phase 1 — Docker Artifact Optimization:
- Replace broad `COPY . .` with targeted frontend source copies (API/Python
changes no longer bust the frontend build cache)
- Replace build-essential with gcc/g++ (leaner runtime)
- Fix LOG_LEVEL=debug → info for production
- Harden .dockerignore (exclude worktrees, IDE, CI, test artifacts)
Phase 2 — State & Persistence:
- Add PUID/PGID support in entrypoint.sh for bind mount compatibility
- Guard against PUID=0/PGID=0 to prevent accidental root execution
- Evict conflicting system users (e.g. node:1000) before UID remap
Phase 3 — Security:
- Always register @fastify/rate-limit so login brute-force protection
works even when global rate limit is disabled (RATE_LIMIT_PER_MIN=0)
- Add trustProxy support (TRUST_PROXY env var, default true) so rate
limiting and audit logs use real client IPs behind reverse proxies
- Strip stack traces from 500 error responses in production
- Fix FSTDEP022 deprecation: maxParamLength → routerOptions
- Add multi-file guard on single-file tool endpoint with clear error
message pointing to the /batch endpoint
Phase 4 — Graceful Degradation:
- Add consolidated hardware detection startup banner (GPU, rate limit,
upload limit, proxy status)
- Add ConnectionMonitor component with health polling and reconnecting
overlay that auto-dismisses when the server comes back
Phase 5 — Deployment Docs:
- Rewrite deployment.md with copy-paste CPU and GPU compose templates
- Add hardware requirements table (minimum, recommended, heavy workloads)
- Add PUID/PGID bind mount documentation
- Add complete env var reference table
- Add reverse proxy guides for Nginx, Nginx Proxy Manager, Traefik,
and Cloudflare Tunnels
Restructure the pipeline builder UX from a cramped sidebar to a
spacious two-pane layout:
- Left pane: categorized, searchable Tool Palette (click-to-add)
- Right pane: Pipeline Canvas with sortable steps, inline action bar,
and collapsible preview panel with before/after slider
- Mobile: full-screen canvas with FAB + bottom sheet for tool selection,
image preview, and thumbnail strip (previously hidden entirely)
- Fix nested <button> HTML violation in SortableStep
- Fix stray semicolon rendering in step cards
Guard startPolling in handleOnline to only fire when transitioning from
offline state. Previously, a spurious browser online event while already
connected would start a polling interval that never gets cleared.
When lazyWithRetry exhausts all retry attempts on a chunk error, also
call setDisconnected() so the reconnecting banner appears alongside the
ErrorBoundary's "Update Available" card.
- Add "unable to preload" pattern to isChunkError for Vite CSS preload failures
- Move ConnectionMonitor and ConnectionBanner outside ErrorBoundary so they
remain visible when the error boundary catches a render crash
- Add test for CSS preload error retry
Fastify's find-my-way router defaults maxParamLength to 100 chars.
Files with long names (e.g., stock photo filenames >100 chars) would
return 404 on download because the :filename param exceeded the limit.
checkHealth previously ignored non-ok responses (e.g. 503), silently
doing nothing when the server reported unhealthy. Add else branch to
transition connected → disconnected on non-ok status. Add tests for
connected → disconnected on fetch failure and offline → reconnected
on health check success.
- Add 8 new E2E specs for AI tools (upscale, enhance-faces, colorize,
restore-photo, erase-object, smart-crop, passport-photo, red-eye-removal)
closing all HIGH/MEDIUM coverage gaps from the test matrix audit
- Fix ensureAiDirs() crash on non-Docker environments by gating on
isDockerEnvironment() — prevents ENOENT when /data doesn't exist
- Bump torch 2.6.0→2.7.0 and torchvision 0.21.0→0.22.0 in feature
manifest for broader Python version compatibility
- Add Python 3.14 version guard warning in install_feature.py
- Remove duplicate torchvision shims from upscale.py and enhance_faces.py
(dispatcher.py already handles this at startup)
- Remove orphaned tools.batch i18n key and dead pipeline-builder filter
- Regenerate 4 visual regression baselines for current UI state
- Add data-testid to passport-photo generate button for E2E testability
The torchvision compatibility shim for basicsr 1.4.2 was missing the
parent-package binding and only proxied a single attribute, causing
upscale and enhance-faces to fail at import time. The fix adds a
__getattr__ proxy for all attributes, binds the shim to the parent
package, and installs it in the dispatcher at startup for defense-in-depth.
Also removes unused anyInstalling variable, redundant `as any` cast,
and applies Biome formatting fixes across the codebase.
- Fix SKIP_MUST_CHANGE_PASSWORD not affecting login/session API responses,
causing frontend redirect even when the env var was set after user creation
- Increase Docker Playwright timeouts (test: 600s, expect: 60s, AI processing: 300s)
to support CPU-only self-hosted environments
- Increase default rate limit from 100 to 50000 req/min for self-hosted deployments
- Fix OCR tests: use filechooser pattern (Dropzone has no static file input),
correct enhance checkbox default, rewrite for actual fixture behavior
- Fix remove-bg tests: update quality labels (Balanced→HD, Best→Max)
- Fix noise-removal skip guard: use waitFor() instead of instant isVisible()
- Fix automate pipeline save test: clean up stale E2E pipelines before assertion
- Add tool-specific suffix to output filenames so downloads don't overwrite originals (batch & single-tool routes)
- Skip deleting shared models when uninstalling a bundle that shares models with another installed bundle
- Auto-detect NVIDIA GPU and swap GPU-only pip packages (onnxruntime-gpu, paddlepaddle-gpu) for CPU equivalents
- Refactor docker-compose with YAML anchors and explicit cpu/gpu profiles
- Add libheif-plugin-x265 to Dockerfile
- Fix install-all queue logic to handle concurrent individual installs and clear stale errors
- Unify playwright docker config to use same test dir with API_URL env var
- Fix flaky e2e selectors, rename Strip Metadata → Remove Metadata, handle collage custom dropzone, improve fallback test image generation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The store set an initial 5% optimistically, then SSE sent real progress
starting from 0%, causing the bar to visibly drop. Now progress only
moves forward — both SSE and polling paths use Math.max to never
regress below the current value.
The bottom-right install indicator showed "1/6 installed" even when
installing a single bundle, implying all 6 would be installed. Now the
counter only appears during "Install All" operations.
Replace FeatureInstallPrompt's local SSE/polling with useFeaturesStore
so install progress, errors, and recovery are handled globally — works
across navigation, logout/login, and partial downloads. Shows fun
progress messages and ETA matching the settings page.
Add compose healthcheck and optional GPU profile (--profile gpu).
Replace static "Preview not available" and "Cannot preview this image
format" messages with a loading spinner in ImageViewer and SplitCanvas,
since HEIC files get server-side conversion and the preview appears
after a few seconds. Also default showDetails to true on /fullscreen.
The home page's file-uploaded view rendered its own tool list without
checking AI feature install status, so download icons disappeared after
uploading a file. Now both Quick Actions and All Tools sections show the
download badge consistently with the sidebar ToolPanel.
Uses XMLHttpRequest upload progress to track image upload (0-80%),
then shows "Processing collage..." for the server-side compositing
phase (80-100%). Progress bar with percentage shown in the preview area.
Adds objectFit property to CellTransform (cover/contain). When set to
"contain", the entire image is shown within the cell with background
color fill. Toggle button in the cell controls toolbar switches between
modes. Server-side rendering handles both modes via Sharp.
- Fix pan: use `movement` (cumulative) instead of `delta` (per-frame)
in useDrag — delta accumulation against static memo was wrong
- Fix wheel zoom: replace useWheel with direct DOM addEventListener
using { passive: false } so preventDefault() works and page doesn't
scroll while zooming
- Fix controls: remove auto-hide timer, keep zoom slider and reset
button visible while cell is selected