- Crash after download completes: "OSError: [Errno 24] No file descriptors available" — Not a file-descriptor leak: musl libc (Alpine base image) caps named POSIX semaphores at 256 per process, and every
multiprocessing.Queuecosts 3 semaphores / everyEventcosts 5, so a config with ~4 path pairs sat at the cap and the first staging→finalMoveProcessafter a download pushed it over (with 6+ pairs the container crashed at startup). Single-producer/single-consumer result queues and wake/terminate events are now backed by pipes (PipeStream/PipeFlag, 0 semaphores each), taking the main process from ~250 semaphores at 4 pairs to ~21 — and ~3 per pair instead of ~51, so large path-pair configs now work.mp.Queueremains only for parent→child dispatch and the multi-process log queue. Pipe ends unused by each process are closed after spawn, so a peer that dies mid-operation now yields EOF instead of a hung read/write — hardening inherited from review, not present in the old queue-based code. (#654)
- Mobile file list scrolling - The file list no longer uses a fixed-size scroll container on mobile, restoring natural page scrolling on small screens; desktop and mobile now share a single responsive file row template. (#370)
- Dependency updates - Updated Angular frontend dependencies (Angular group, TypeScript ESLint, ESLint, Font Awesome, and transitive packages), website dependencies (PostCSS, React DOM, webpack-dev-server, and others), and GitHub Actions (setup-python 7, setup-uv 9, docker/login-action 4.5.1).
- Dependabot grouping - Docusaurus dependency updates are now grouped into a single Dependabot PR.
- Dependency and workflow updates - Updated Angular frontend dependencies, TypeScript ESLint, GitHub Actions, and website dependencies merged through develop.
- Dead worker result preservation - Extract/validate worker results emitted immediately before a crash are now drained and buffered before dead worker queues are closed, so completed and failed results are surfaced exactly once after supervisor recovery. (#571, #590)
First stable release. This milestone bundles the optional FTPS transfer protocol, a large backend reliability and persistence hardening pass, frontend performance work, the Angular 22 upgrade, and a security-focused test and runtime baseline.
- Optional FTPS transfer protocol — A new
protocolsetting (sftpby default, orftps) lets transfers run over FTPS (FTP over TLS) instead of SFTP, reaching substantially higher throughput on high-latency links by sidestepping the SSH layer. FTPS mode is a hybrid: file discovery always stays on SSH (newremote_ftp_port, default21, is used only for FTPS transfers; the existing remote port remains the SSH/SFTP port). FTPS transfers use the same persisted Connections settings as SFTP — what the Settings UI shows is what lftp runs. The data channel is always TLS-encrypted; certificate verification is exposed viaftp_ssl_verify_certificateand defaults off (with a per-connectionWARNINGlog) to accommodate self-signed seedbox certificates — documented as an encryption-without-authentication trade-off with remediation. FTPS options live in a dedicated Transfer Protocol section of the Settings page. Existing SFTP installs are unaffected. (#485) - Notify on download start — New
notify_on_download_startoption (disabled by default) emits adownload_startevent when a file enters theDOWNLOADINGstate. Fires through the existing webhook, Discord, and Telegram channels, with a yellow Discord embed color and "Download Started" label. (#486) - Automatic extract/validate worker recovery — A crashed extract or validate worker subprocess is now detected (
is_alive()/exitcode) and recreated automatically via a sharedWorkerSupervisor, so extraction/validation resumes after a transient fault without restarting the container. (#535) - Failed-move surfacing + in-session retry — A failed staging→final move is now shown as a distinct
MOVE_FAILEDstate (with a "Move failed" badge in the UI) instead of silently appearing done, and is retried within the session up to a bounded per-file budget. (#536)
- Angular 22 upgrade — Upgraded the frontend from Angular 21 to 22 (all
@angular/*packages,@angular/cli,@angular/build,@angular/cdk, andangular-eslintto 22.0.0), bumping TypeScript to 6.0. Migrated the deprecatedAPP_INITIALIZERtoken to the functionalprovideAppInitializer()and made the implicit zoneless contract explicit withprovideZonelessChangeDetection(). Supersedes the closed Dependabot PRs #550 (TypeScript 6 ERESOLVE) and #551 (@angular/cli22 ERESOLVE) by performing the lockstep major bump atomically. (#556) - Performance — backend model build — Replaced the O(n²) per-cycle model build with O(1) BFS frontiers and a skipped child-list copy in
model_builder. (#520) - Performance — frontend file list — Coalesced SSE-driven view rebuilds, memoized the filter, and made bulk-remove a single pass to eliminate file-list rebuild thrash. (#521)
- Performance — logs page — Capped the live-log buffer (ring buffer + stable collision-free
trackBy), then rendered it with CDK virtual scroll and batched change detection to ~one update per animation frame. (#522, #539) - Refactor — lftp parser — Decomposed the 412-line, complexity-48
LftpJobStatusParser.__parse_jobsinto focused, strictly-typed helpers. (#523) - Refactor — ViewFileService — Split the god service into a capabilities module, a selection service, and a command service (
ViewFileCommandService), with a single source of truth for the status→capability state machine. (#524, #541) - Refactor — backend coupling & service conventions — Decoupled
Controller/CommandPipeline/persist-sync, made config option types typo-safe, and migratedConfigService.set/AutoQueueServiceto the documented tap-based mutating-service contract. (#525, #542) - Repo & CI hygiene —
.gitignorecleanup, pinned GitHub Actions, per-job timeouts, removal of dead scripts, and re-enabled the mock-based lftp/ssh/scanner unit tests in CI. (#527, #529) - Dependency updates — Angular, React 19.2.x, typescript-eslint, eslint, vitest, hono, qs, postcss, webpack-dev-server, webob, shell-quote, and
astral-sh/setup-uv, among others. (#488–#506, #545–#554, #560, #565)
- Remote scan timed out at 30s under password auth — The SSH password-prompt wait in
Sshcphad no explicit timeout, so it silently used pexpect's 30s default instead of the intended 180s. A server slow to present the prompt (e.g. a reverse-DNS/GSSAPI auth delay on a cold connection) failed scan attempts even though the log reported a 180s limit. The prompt wait now uses the full command timeout. Downloads were unaffected (separate LFTP path); the retry logic masked the failure as recurring log errors. (#562) - Config handler persistence atomicity —
/server/config/setnow captures the pre-mutation value, attempts the disk write, and onOSErrorrolls back the in-memory state and returns a structured HTTP 500 instead of letting the exception bubble up as a stack trace. The LFTP hot-reload callback only fires after a successful write, preventing the runtime from being reconfigured to a value that never made it to disk. (#469) - Integration delete persistence ordering —
/server/integrations/<id>DELETE now writespath_pairs.jsonbeforeintegrations.json. A crash between the two writes previously left a danglingarr_target_id(instance gone from integrations but still referenced from a path pair), which is rejected by cross-validation on next load. The new order downgrades the worst-case crash outcome to a harmless orphaned instance. (#496) - Auto-queue and path-pairs handlers wrap persistence in try/except —
/server/autoqueue/{add,remove}and/server/pathpairs/{create,update,delete}now return a controlled HTTP 500 with a structured body when the underlyingto_fileraisesOSError, instead of leaking a stack trace. For path-pairs the in-memory mutation is retained (and may diverge from disk in the failure case); the auto-queue handlers were later changed to roll back on failure (see #518). (#497) - Config values containing
%crashed persistence —Config.from_str/to_strnow useConfigParser(interpolation=None), so a%in any value (passwords, exclude patterns, rate limits) survives the write/read round-trip instead of crashing on the default interpolation (#530). The config handler also rolls back its in-memory state on any persist failure, not justOSError. (#507) Controller.exit()leaked worker subprocesses and queue FDs when lftp shutdown errored — teardown is now best-effort and each worker join is bounded. (#508)- Auto-queue thread-safety — Cross-thread set/list races in
AutoQueue/AutoQueuePersistare now lock-guarded. (#509) - Move pipeline silent data loss — Failed staging→final moves were reported as success; failures now propagate via a result queue. (#510)
- Worker crash took down the whole download engine — A Validate/Extract subprocess crash is now isolated from the controller loop. (#511)
- Failed periodic
persist()misclassified as a clean shutdown — now surfaced at ERROR instead of only INFO. (#512) - lftp job-status parser robustness — Fixed three edge-case parsing bugs (#517) and a pget header with no data line that consumed the following job's header (#555).
- Auto-queue add/remove applied its side effect even when persistence failed — now rolls back and returns a structured 500. (#518)
- Directory validation broke on tilde/relative remote paths — fixed tilde expansion in the remote find/hash commands. (#519)
- Web command handlers could block the request thread indefinitely — now bounded with a 504 timeout. (#526)
- File-action failures were silently swallowed in the UI — single-file action errors now surface and stuck rows recover. (#513)
- UI locked out after reload when
web.api_keywas set — fixed the SSE 401 deadlock by bootstrapping config off the stream. (#514) - Bulk Delete Local/Remote had no confirmation — now requires a two-click confirm to guard against accidental data loss. (#515)
- Unguarded
JSON.parsein SSE/logs/auto-queue handlers — wrapped with try/catch and surfaced fetch errors. (#516) - Cross-device move raced lftp's temp-file rename — The staging→final copy fallback could list a
<name>.lftptemp file, have lftp rename it to the final name mid-copy, then fail withENOENTand crashMoveProcesswith an uncaughtshutil.Error. The move now defers (recoverable retry) while any*.lftptemp files remain in the staging tree, and wraps the copy so a rename in the narrow remaining window reports a recoverable failure and keeps the source intact instead of crashing the controller. (#567) - Integrations handlers wrap persistence in try/except —
/server/integrationscreate/update/delete now return a controlled HTTP 500 with a structured JSON body when the underlyingto_fileraisesOSError, instead of leaking an unhandled stack trace. Matches the path-pairs handler contract via a shared_persist_or_500helper. (#530) - Version-check banner could hang on a stalled GitHub connection — The "new version available" check now bounds the GitHub release request with a 10s timeout and recovers on error, instead of leaving it open indefinitely (browsers apply no default timeout). The banner is best-effort, so a slow or dropped connection now fails quietly. (#527)
- Documented hardened Docker runtime baseline — The shipped
docker-compose.dev.ymland the installation docs now recommend running the container withno-new-privilegesandcap_drop: ALL, adding back only the capabilities the entrypoint needs (CHOWN,SETUID,SETGID,DAC_OVERRIDE,FOWNER). These flags are opt-in for users running via plaindocker runor their own compose template. (#528) - Web API key no longer persisted client-side — kept out of browser storage. (#514)
- Security-critical path test coverage — Added tests for extraction zip-slip, SSH command quoting, persist races, config special characters, scanner symlinks, and webhook URL-scheme validation. (#530)
- Discord webhook 403 Forbidden —
format_discordonly setContent-Type, so urllib filled in its defaultPython-urllib/x.yUser-Agent, which Discord blocks. Webhooks now identify themselves asseedsync (https://github.com/nitrobass24/seedsync). Same headers dict serves both the Test button and the live notifier, so all Discord paths are covered. (#483, #484)
- Dependency updates — react and react-dom 19.2.5 → 19.2.6 (#478, #479), Angular group +10 packages (#480), typescript-eslint 8.59.0 → 8.59.2 (#481), @babel/plugin-transform-modules-systemjs 7.29.4 (#477), fast-uri 3.1.0 → 3.1.2 (#475, #476), hono 4.12.14 → 4.12.18 (#474), ip-address and express-rate-limit (#473).
- Image size reduced from 114 MB to 64 MB —
RUN --mount=from=ghcr.io/astral-sh/uvkeepsuvout of the runtime image; build artifacts no longer persist (#437) - Dockerfile collapsed to 2 stages on Python 3.13-alpine — Removes an intermediate stage and the legacy Buster build path (#436)
- Test image rewritten as Alpine with Python 3.13 (#435)
- Removed stale Docker test infrastructure — Old Compose files and fixture data carried over from the Protractor era (#434)
- Playwright E2E migrated to the official
mcr.microsoft.com/playwrightcontainer — Eliminates host-side browser install andapt installof system deps; image tag auto-pinned to the@playwright/testversion frompackage-lock.json(#456) - Build and Test (amd64) runs on develop pushes — Populates the GHA cache so PRs against develop hit a warm cache instead of paying the install cost on every run (#456)
- Playwright browser cache + npm cache wired through
actions/cache(#452, #453) - Python 3.12 → 3.13 in CI and Dockerfile (#451)
- README refreshed to cover features through v0.17.0 — Notifications, Sonarr/Radarr, integrity verification, staging, API key (#454)
- Test count badges added to the README (#451)
- Dependency updates — Angular group (10 packages), typescript-eslint, jsdom, eslint, Docusaurus 3.10.0 → 3.10.1 across 5 packages (#458, #459, #460, #461, #463, #464, #465, #466)
- LFTP hot-reload — Connection-related settings (parallel connections, max total connections, socket buffer size, bandwidth limit) apply without a container restart. Settings UI distinguishes between options that take effect immediately and those that require restart (#433)
- Atomic config writes —
Config.to_file()now flushes to disk via temp file +os.renameso a process kill mid-write can't truncate the config (#433) - Expanded unit and E2E test coverage:
- 47 security middleware unit tests (#439)
- 36 controller core unit tests (#444)
- 28 FileOptions / Integrations / Option component tests (#448)
- 25 AutoQueueService and PathPairsService tests (#445)
- 20 HeaderComponent and VersionCheckService tests (#443)
- 18 ViewFileFilterService tests (#440)
- E2E for integrations CRUD (#441)
- E2E for File Actions & Error States (#438)
- E2E Settings coverage expansion (#442)
- Web App Job & Context Python tests (#446)
- Handler integration test expansion (#447)
- Python integration tests added to CI (#449)
- Hash Algorithm select test flake — Test waited on the wrong config field; the algorithm select's disable gate is
validate.enabled, notvalidate.xfer_verify. Test now sets the correct field and waits for the SSE-delivered model value before asserting (#455) - StreamHandler leaks in test setUp methods — Tests added a handler to the shared root logger but never removed it. Loggers are singletons, so by test N the logger had N handlers and each log line printed N times. Fixed via
self.addCleanup(logger.removeHandler, handler)across 8 test files / 10 setUp methods (#450, #457) - Multiprocessing resource leak in
test_active_scanner.py—scanner.close()now registered withaddCleanupso resources release even if assertions raise
apkand its package database are stripped from the runtime image —/sbin/apk,/etc/apk,/var/cache/apk,/lib/apk, and thelibapkshared libraries are removed in the runtime stage to keep the image under 64 MB (#437). This means in-image vulnerability scanners (Trivy, Grype) can't enumerate Alpine packages directly from a running container; consumers should scan the publishedghcr.io/nitrobass24/seedsyncimage via SBOM or image-history tooling. Affected runtime packages:lftp,openssh-client,ca-certificates,setpriv,libstdc++. Derived images that needapk addshould base on Alpine and reinstall what they need rather than extending this image.
- Multi-instance Sonarr/Radarr integration — Replace single Sonarr/Radarr config with named instances stored in
integrations.json; each path pair can be mapped to specific *arr instances viaarr_target_ids; full CRUD REST API and chip-picker UI for attaching/detaching instances (#425, #426) - Discord notification preset — Configure a Discord webhook URL in Settings to receive rich embed notifications with color-coded event types (download complete, extraction complete/failed, delete complete); includes Test button (#329, #428)
- Telegram notification preset — Configure a Telegram bot token and chat ID in Settings to receive Markdown-formatted notifications; includes Test button (#329, #428)
- Cross-validation for arr_target_ids — Path pair create/update rejects unknown integration instance IDs with a 400 response (#426)
- Notification architecture — Refactored
WebhookNotifierwith extracted_resolve_event_typeand generic_fire_rawdispatch; Discord and Telegram formatters are pure functions innotification_formatters.py(#428) - Controller decomposition — Decomposed
step(),_update_pair_model_state(), andupdate()into focused methods, reducing cyclomatic complexity below C901 threshold (#419, #420) - Settings UI consistency — Integrations and Path Pairs headers use matching
.headerpattern; dark mode card headers use consistent light text color (#429, #430, #431) - Semantic headings — Integrations and Path Pairs section titles now use
<h3>for screen-reader heading navigation (#430) - Dependency updates — Angular group (10 updates), vitest 4.1.4→4.1.5, typescript-eslint 8.56.1→8.59.0, postcss 8.5.6→8.5.12 (#421, #422, #423, #424)
- Corrupt integrations.json recovery — Corrupt file now returns empty config instead of re-running migration with new UUIDs, which could overwrite user customizations (#426)
- Refresh preserves instances on error —
IntegrationsService.refresh()preserves the existing instance list on HTTP error instead of replacing with empty (#426) - Toggle enabled error handling —
onToggleEnabled()now shows an error message and refreshes on failure instead of silently leaving the UI out of sync (#426) - Arr-picker dark mode — Fixed white dropdown background in dark mode caused by CSS specificity mismatch (#430)
- Telegram Markdown safety — Backticks in filenames are escaped to prevent breaking inline code spans (#428)
- Webhook URL redaction —
webhook_urladded tosensitive_property_names()for API response redaction (#428) - Thread leak on start failure —
_fire_rawnow cleans up_active_threadsifthread.start()raisesRuntimeError(#428)
- Credential redaction — Discord webhook URLs and Telegram bot tokens are redacted in API responses via
sensitive_property_names()(#428) - URL scheme validation — Notification test endpoints reject non-http(s) URLs before attempting the request (#428)
- Button type safety — All integrations component buttons have explicit
type="button"to prevent unintended form submission (#426)
- Sonarr/Radarr integration — Test connection buttons in Settings to verify Sonarr and Radarr API connectivity (#328, #362)
- Angular ESLint enforced in CI — All 17 lint rules at
errorwith--max-warnings 0; covers OnPush, a11y, type safety, and code style (#376-#382, #389-#392)
- Mobile file list scrolling — Fixed items skipping during scroll on Android/iOS by correcting virtual scroll
itemSize(50→82) and switching to dynamic viewport height measurement via ResizeObserver (#370, #371, #396) - Log streaming memory — Stream log files line-by-line instead of
readlines()to avoid loading entire log into memory (#385) - Controller deadlock on exception — Use
withstatement on model lock to prevent deadlock when exception occurs during model update (#373, #384) - Integrations error leaking details — Strip internal exception details from Sonarr/Radarr error responses (#386)
- Accessibility — 55 template a11y violations fixed:
<div>/<a>click targets converted to<button>, keyboard navigation added, decorative images marked withalt="" aria-hidden="true"(#391) - OnPush change detection — All components now use
ChangeDetectionStrategy.OnPushwith Angular signals (#392) - Type safety — Replaced 50
no-explicit-anyviolations with proper types; addedModelFileJson,ConfigValue,OptionValueinterfaces (#390) - Dependency updates — Angular group (8 packages), follow-redirects, hono, vitest, ruff, pytest, pexpect, tblib, Docusaurus (#347-#368, #388)
- npm audit fixes — Resolved high-severity vulnerabilities in Angular dependencies (#388)
- Virtual scrolling for large file lists — Improves performance when browsing directories with many files (#335)
- Size sorting in file list — New sort option to order files by size (#334)
- Log level dropdown — Replaced debug toggle with a full log level selector (DEBUG/INFO/WARNING/ERROR/CRITICAL) (#332)
- Bulk delete crash — Fixed unbounded process spawning that caused crashes during bulk delete; scoped concurrency cap to delete operations and added move retry on failure (#338, #341)
- Infinite busy-loop in command process throttling — Fixed CPU spin in the controller process throttle logic
- Mobile viewport height — File list viewport no longer wastes 40px on small screens where the header row is hidden
- Bulk action error resilience — A single failed action no longer aborts the entire bulk operation pipeline
- Dependency updates — Angular group bump (9 packages), hono, @hono/node-server, jsdom, vitest, lodash (#326, #333, #336, #337, #343, #344, #345)
- Documentation — Moved recommended hard-link workflow from FAQ to a dedicated Usage section; fixed UI label consistency (#340)
- Pyright strict mode — Upgraded Pyright type checking from basic to strict mode across the entire Python codebase; added type stubs for bottle, pexpect, and tblib (#291, #316)
- Extraction-move validation race — Extraction completion no longer spawns a staging move while validation is still running; the move is deferred to the validation completion path (#316)
- Directory size preservation in exclude filter —
_filter_childrennow preserves scanner-reported directory sizes instead of recomputing from filtered children, preventingremote_sizedivergence (#316)
- Website dependency bumps — Fixed 4 vulnerabilities in website transitive dependencies: brace-expansion, path-to-regexp, picomatch, minimatch (#324)
- Pygments 2.20.0 — Bumped pygments to fix ReDoS vulnerability GHSA-5239-wwwm-4pmq (#325)
- Configurable remote Python path — New "Remote Python Path" setting lets users specify a custom Python binary on the remote server (e.g.
~/python3/bin/python3), solving issues on seedboxes wherepython3is not on the default PATH (#314, #315) - Startup config validation — SeedSync now validates required LFTP config fields on startup and shows clear error messages when
remote_address,remote_username,remote_path, orlocal_pathare missing (#310, #313)
- scan_fs.py Python 3.5 compatibility — Remote scanner script now works on servers with Python 3.5+ by using
typing.List/Optionalinstead of modern type syntax (#314, #315) - Sshcp.copy() user@host format — Fixed
scpcommand construction to use the shared_remote_address()helper, matching the pattern used byshell()(#312) - Pre-existing code quality bugs — Fixed extraction retry counter, NotImplementedError base class, and process cleanup issues found during Ruff review (#289, #312)
- uv for Python dependencies — Replaced pip + requirements.txt with uv and PEP 621 dependency groups; pinned uv versions in CI and Docker (#286, #311)
- Playwright E2E test suite — Replaced legacy Protractor tests with Playwright; 55 tests covering all pages, navigation, themes, and settings (#250)
- Pyright type checking enforced in CI — Completed Pyright phases 3 & 4, fixing 166 type errors to reach 0 errors in basic mode; Pyright check is now required in CI (#249)
- ModelFile nullable size fields —
local_sizeandremote_sizeare now correctly typed asnumber | nullto match the Python backend JSON contract
- Reject control characters in filenames — Decoded filenames containing control characters are now rejected to prevent corrupted file entries and queue command injection (#300)
- Redact sensitive credentials from API — SSH password and key passphrase are no longer exposed in API responses; set handler rejects the redacted sentinel value (#257)
- Angular dependency updates — Bumped Angular group to latest, jsdom to 29.0.1 (#307, #308)
- CI: actions/setup-python v6 — Bumped setup-python action from v5 to v6 (#306)
- Legacy Docker E2E infrastructure — Cleaned up Protractor Docker Compose files, test images, and fixture data
- Parser crash on long filenames — Chunk progress lines that wrap across PTY boundaries no longer crash the controller; unrecognized lines inside a job context are skipped with a warning (#290, #293)
- False download completion on parser error — Parser failures no longer trigger false "download completed" signals that left files stuck in staging at partial progress (#296)
- Progress tracking with .lftp temp naming — ActiveScanner now recognizes
.lftp-suffixed temp files in staging, fixing 0% progress display for single-file downloads when temp naming is enabled (#298)
- PEP 621 pyproject.toml — Consolidated Python dependencies from Poetry format to PEP 621 with proper dependency groups (runtime, test, dev); removed dead dependencies (#287)
- Ruff linting and formatting — Added Ruff as Python linter/formatter with CI enforcement; applied auto-fixes across 99 files (#288)
- Pyright type checking — Added Pyright in basic mode with CI reporting; fixed 91 type errors across 26 modules (#292, #295)
- Python unit tests in CI — Python tests now run in CI alongside Angular tests (#287)
- Angular dependency updates — Bumped Angular group to 21.2.4, vitest to 4.1.0, jsdom to 29.0.0 (#282, #283, #284)
- Inline transfer verification — LFTP's
xfer:verifychecksums files during download, catching corruption in real-time; enabled by default (#242) - Post-download integrity checking — Optional validation step that compares local and remote checksums via SSH after download, with per-file Validate button and Validated/Corrupt status indicators (#125, #276)
- File list pair label column — File list shows which path pair each file belongs to (#156)
- Accessibility — All file action buttons converted to native
<button>elements (#241) - Comprehensive test coverage — 287 unit tests across Angular (Vitest) and Python (pytest) (#225)
- Alpine-only Docker image — Removed Debian variant; all images are now Alpine-based (~45 MB), multi-arch (amd64/arm64) (#231, #244)
- Multiprocessing fork to spawn — Fixes Python 3.12 deprecation warnings (#228)
- Verbose logging in web UI — Verbose LFTP logging setting is now exposed in the Settings page under Logging (#266)
- CI deduplication — Eliminated redundant amd64 Docker build; publish triggers now build, test, and push in a single job (#274)
- Stopped download delete stuck — Deleting local files for a stopped download no longer leaves the UI spinner and ActiveScanner polling forever (#271, #272, #273)
- File descriptor leak on restart — Multiprocessing Event/Queue references are now released in
close_queues()to prevent FD exhaustion across restarts (#265) - Model rebuild log noise — Temporary model objects used during diff computation no longer emit "Adding file" log messages on every controller loop iteration (#267)
- Exclude patterns use wrong LFTP flag — Changed
--exclude(regex) to--exclude-glob(glob) so patterns like*.nfowork correctly instead of being silently misinterpreted as regex (#271)
- LFTP queue command logging — Log LFTP queue commands at INFO level (previously DEBUG) for easier troubleshooting without enabling verbose mode
- CI workflow_dispatch support — Allow manual Docker image publishing via GitHub Actions workflow_dispatch trigger
- Exclude patterns not passed to LFTP — Exclude patterns were only filtering the UI display model but were never passed to LFTP's
mirrorcommand, causing all files to be downloaded regardless of configured exclusions (#259) - Parser crash on Unraid PTY line-wrap fragments — Unraid's Docker PTY handling wraps long LFTP progress lines, producing tail fragments like
/s eta:25m [Receiving data]that crashed the parser and stopped the container (#260) - Parser error threshold too aggressive — Bumped consecutive status error threshold from 2 to 10 so persistent parse issues don't crash the app within seconds
- PTY width override on Unraid — Set
COLUMNS=10000in pexpect spawn environment as belt-and-suspenders alongsidesetwinsizeto prevent Unraid from overriding PTY dimensions
- lftp parser crash on orphan progress lines — Parser no longer crashes when lftp emits bare progress lines like
3.0K/s eta:3m [Receiving data]outside a job context, which caused the Docker container to stop (#253)
- Recursive exclude filtering — Exclude patterns now filter through nested children, not just top-level files (#158, #217)
- Persist key separator — Replace colon separator with non-printable unit separator (
\x1f) to avoid conflicts with filenames containing colons; includes backward-compatible migration of existing persist data (#221) - Default-pair filenames with colons — Fix
_sync_persist_to_all_buildersincorrectly excluding filenames likeShow 01:02.mkvfrom default pair contexts (#221) - Python version check placement — Move version guard above project imports so the friendly error message displays on older Python versions (#224)
- Angular memory leaks — Fix 6 leaked subscriptions in header and sidebar components (#220)
- LZIP archive detection —
is_archive()now recognizes LZIP format with 5-byte magic signature (#218) - Plain gzip/bzip2 extraction — Support extraction of standalone
.gzand.bz2files (not just.tar.gz/.tar.bz2) (#219) - Webhook graceful shutdown —
WebhookNotifier.shutdown()drains in-flight webhook threads with a configurable timeout, preventing lost notifications on exit (#222) - 67 new tests — Unit tests for extraction, exclude patterns, webhook drain, persist migration, file-list component, bulk-action-bar component, and path pairs CRUD handler (#219, #226, #227)
- Angular subscription cleanup — Migrate 6 components from manual
Subscription[]/ngOnDestroytotakeUntilDestroyedpattern (#220) - Shared form template — Extract duplicated path-pairs form fields into
ng-templatewith*ngTemplateOutlet(#223) - Defensive copy for path pairs —
PathPairsConfig.get_pair()returns deep copies to prevent external mutation of internal state (#221) - Dependency cleanup — Move
timeout-decoratorfrom runtime to test-only; remove unused localization strings (#224) - Angular dependency updates — Bump Angular group dependencies (#213)
- RAR archive extraction — Use source-built 7-Zip 26.00 with RAR codec support; distro packages strip the proprietary RAR codec (#204, #210, #212)
- Extract/move pipeline stall — Fix
pending_completionnever clearing for files in EXTRACTED or EXTRACT_FAILED state when staging is enabled (#208) - Auto-delete with path pairs — Fix auto-queue commands missing
pair_idfor path pair configurations (#205) - Pre-extraction false negatives — Remove redundant archive verification that rejected valid archives (#207)
- Pre-built 7-Zip Docker image — Extract 7zz binary from
ghcr.io/nitrobass24/docker-7zipinstead of compiling from source during CI (#212)
- CodeQL alerts resolved — Fix code scanning alerts identified by GitHub CodeQL (#197)
- Multiple path pairs — Configure multiple remote/local directory pairs, each with independent LFTP and scanner instances. Replaces the single remote/local path with a flexible pair-based model (#122, #149, #155, #161)
- Path pairs settings UI — Full CRUD interface for managing path pairs in Settings, with per-pair enable/disable and auto-queue toggles (#160, #162, #163)
- Exclude patterns — Filter out unwanted remote files using glob patterns (e.g.
*.nfo,Sample/), configurable per path pair (#26, #146) - Multi-select and bulk operations — Select multiple files and apply queue/stop/delete actions in bulk (#123)
- Webhook notifications — HTTP POST notifications on file download/extract events (#128)
- Historical log query —
/server/logsendpoint with search, filter, and level controls; accessible from the UI (#124) - Structured JSON logging — Optional JSON log format for log aggregation tools (#127)
- Alpine Docker image — Lightweight Alpine variant alongside the Debian image, published as
*-alpinetags (#164) - Docker HEALTHCHECK — Built-in health check for container orchestrators with
WEB_PORTenv var support (#164, #180)
- Python scanfs replaces PyInstaller binary — Remote scanner is now a plain Python script, eliminating glibc compatibility issues with seedbox servers (#80, #148)
- JSON serialization for scanfs — Scanner uses JSON instead of legacy serialization for safer, more debuggable output (#129)
- Consolidated extraction to 7z — All archive extraction (zip, rar, 7z, tar, gz, bz2, xz) now uses the single
7zbinary, removing theunrardependency and reducing image size (#178) - Dual-image CI pipeline — CI builds and tests both Debian and Alpine variants on every push, with parallel arm64 builds on develop (#164, #175, #176)
- Startup log improvements — Path pairs dumped at startup for debugging; model logs show short pair ID instead of full GUID (#165)
- Per-pair extraction pipeline — Extraction now uses the correct path pair's filesystem paths instead of always using the first pair (#167, #173)
- Per-pair staging subdirectories — Each path pair gets its own staging subdirectory, preventing filename collisions (#168, #173)
- Unique pair name enforcement — Duplicate pair names are now rejected on create/update (#169, #172)
- Graceful pause when all pairs disabled — Controller idles cleanly and the UI shows an informational banner instead of falling back to legacy behavior (#170, #174)
- Spurious staging moves on restart — Files already moved to their final location are no longer re-queued for move on container restart (#177, #179)
- Healthcheck IPv6 resolution — Healthcheck uses
127.0.0.1explicitly instead oflocalhost, which resolved to IPv6::1on Alpine (#180)
- paste WSGI server — Replaced with Bottle's built-in multithreaded server (#140)
- patool dependency — Archive extraction consolidated to 7z (#141, #145, #178)
- unrar dependency — Removed in favor of 7z which handles all RAR formats (#178)
- API key authentication — Optional API key protects all
/server/*endpoints. Set via Settings > Web > API Key. SSE stream accepts key as query parameter; config GET is exempt for frontend bootstrapping (#130) - Security response headers — Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy on all responses (#130)
- CSRF protection — Origin/Referer validation on state-changing requests with loopback exemption (#130)
- Rate limiting — Per-IP sliding window (120 requests/60s) on all endpoints except SSE stream (#130)
- Config file backup — Automatic backup before each config save, keeps last 10 with ISO timestamps (#130)
- Scanner home directory fallback — Automatically retries scanner installation to
~/when/tmpis restricted on the remote server (#114) - Server Script Path documentation — README troubleshooting, configuration docs, and FAQ entries for common scanner installation issues (#114, #115)
- CSP blocks inline scripts — Moved inline theme detection to external
theme-init.jsand disabled Beasties critical CSS inlining to comply withscript-src 'self'(#134) - Settings page not loading — Eagerly initialize ConfigService and AutoQueueService in APP_INITIALIZER to ensure config loads on first connection (#136)
- SSE API key state mismatch — Clear SSE stream API key at all config-reset paths (disconnect, parse failure, fetch failure) to prevent stale auth (#136)
- SFTP permission preservation overrides umask — Disabled lftp
sftp:set-permissionsso local umask applies to downloaded files (#115) - Scanner path traversal in delete endpoints — Validate filenames and check path containment before local/remote delete operations (#130)
- Zip-slip archive extraction — Pre-validate zip/tar members for symlinks and path traversal; post-extraction check for rar/7z formats (#130)
- Remote scanner shell-quoting — Properly escape remote paths in md5sum and execution commands (#114)
- Path traversal protection on all controller command endpoints (queue, stop, extract, delete) (#130)
- Constant-time API key comparison with
secrets.compare_digest(#130) - Null byte rejection in filename validation (#130)
- UMASK diagnostic logging — Log the applied umask and previous value at startup to help diagnose permission issues (#109)
- Staging path crash on Unraid — Container failed to start with
PermissionError: /stagingwhen staging was enabled but/stagingwas not a writable volume mount. The/stagingdirectory is now created and chowned at container startup, declared as a Docker VOLUME, and exposed in the Unraid template as an optional advanced path (#109) - Entrypoint permission errors hidden —
chownfailures for/downloadsand/stagingwere silently swallowed. Errors now surface on stderr. A writability check (run as the app user) verifies/downloadson every startup and/stagingwhen it is externally mounted, exiting with a clear error message instead of crashing deep in the application
- UMASK not applied to downloaded files — Shell umask was not reliably inherited by lftp through the setpriv exec chain. Now applied directly via
os.umask()in Python at startup, ensuring correct file permissions for all downloaded files (#109)
- Unraid installation docs — Added Unraid Community Applications install instructions to README and Docusaurus docs site (#79)
- Docker build optimization — Added
.dockerignore, reduced build context, shared build artifacts between CI jobs - CI improvements — Test caching, always-rebuild test image, non-root test user
- Poll interval tuning — Reduced unnecessary polling in logger and web app
- Delete Remote directory hang — Fixed infinite spinner when deleting a remote directory (#92)
- SSH password exposed in debug logs — Mask
remote_passwordin debug config dump output (#97)
- UMASK environment variable — Control downloaded file permissions via
UMASKenv var (e.g.002for 775/664) - Angular unit tests in CI — 132 Vitest unit tests now run on every CI build (#93)
- Parallel multi-arch Docker builds — amd64 and arm64 images now build in parallel and merge into a single multi-arch manifest, reducing total release build time (#93)
- Skip Angular build on arm64 — Build Angular natively and inject into arm64 Docker build via
build-contexts, cutting arm64 CI from ~4min to ~1min - Angular packages — Bumped from 21.1.x to 21.2.0
- Dependabot alerts — Dismissed 4 dev-only alerts; remaining 5 are unfixable Docusaurus transitive deps (#96)
- RAR5 extraction failure — Replace
unrar-free(RAR1-3 only) with RARLAB's fullunrar(v6.21) which supports all RAR formats including RAR5. Modern archives use RAR5, causing silent extraction failures and a download-extract-fail-delete loop (#84) - Incomplete directory stuck in DOWNLOADED — Fix persist authority overriding children BFS check when extra local files inflate
local_size >= remote_size, preventing re-download of missing remote children (#83) - LFTP chunk parser crash — Fix crash on rangeless
\chunklines (e.g.\chunk 5077) emitted by LFTP during parallel downloads - Zombie app after controller crash — Fix main loop swallowing controller exceptions, causing the app to run indefinitely without functioning instead of cleanly exiting for Docker restart
- Extract retry loop not stopping — Fix retry counter being reset on every re-download cycle, preventing files from reaching EXTRACT_FAILED state after 3 attempts
- Delete Local fails for staged files — Check staging path before local_path in DELETE_LOCAL so files still in staging can be deleted
- Late-binding closure in DELETE_LOCAL — Bind
delete_pathas default argument so each callback captures the correct path when multiple deletes are batched - Manual extract silently failing — Extract dispatch errors (e.g. "no archives found") were silently swallowed, leaving the file in DOWNLOADED state with no UI feedback. Now reported as failures so the file reaches EXTRACT_FAILED state
- Extract fails after staging move — When staging is enabled and files are moved to the final local path before extraction runs, ExtractDispatch only checked the staging path. Now falls back to local_path so archives are found in either location
- SSH output decoding crash — Fix
UnicodeDecodeErrorcrash when remote commands (e.g. scanfs) return output containing non-UTF-8 bytes (#76)
- Extraction failure handling — When extraction fails, SeedSync now retries up to 3 times by re-downloading the file. After 3 failures, the file shows a red extracting icon with "Extract failed" text so the user knows intervention is needed. Extract, Delete Local, and Delete Remote actions are all available from this state. (#70, #71, #72, #73)
- Persist cleanup during staging moves — Fix persist cleanup incorrectly firing for files with in-flight staging moves, which caused state loss
- Extraction timing with staging — Fix extraction starting before the staging move completed
- LFTP parser crash on long paths — Fix crash when terminal line wrapping splits LFTP output mid-line (#69)
- Directory download ETAs — Fix wildly inaccurate ETAs for directory downloads by using LFTP's real-time transfer sizes and EMA smoothing (#68)
- scanfs timeout crash — Fix pexpect resource leak, add retry logic, and enable SSH keepalive (#62)
- SSH crash on apostrophes in filenames — Fix shell crash on filenames like "Don't Look Now" (#66)
- LFTP status parser crash — Fix crash from interleaved 'jobs -v' command echo in LFTP output (#66)
- Error handling hardening — Fix multiple error handling bugs found during code review (#63, #64)
- Cross-device move crash — Fix FileExistsError during staging-to-final moves across filesystems
- Progress bar glitch during downloads — Fix race condition where the UI progress bar randomly dropped to 0% mid-download because active file sizes were wiped when the local scanner replaced its file dict (#57)
- Download state loss after LFTP completion — Fix download state being lost after LFTP job completion (#55)
- LFTP pexpect timeout recovery — Fix lftp pexpect timeout recovery to prevent buffer corruption (#54)
- Staging directory — Downloads land on a fast staging disk (e.g. NVMe/SSD) first, then automatically move to the final destination after completion. (#36)
- Dark mode — Theme toggle with a 3-surface elevation system and semantic color tokens. (#37, #51)
- Advanced LFTP settings — Exposes tunable LFTP options in Settings — parallel connections, max total connections (0 = unlimited), and socket buffer size with human-readable suffixes (
8M,16M). (#40, #44) - Remote server diagnostics — Runs diagnostics against the remote seedbox on first connection, surfacing server environment info. (#41)
- Graceful config upgrades — New config keys are automatically backfilled with defaults and saved to disk — no manual edits needed after upgrading. (#45)
- Settings layout — Moved File Discovery below Archive Extraction in the left column. (#48)
- Fix crash when a remote file is removed during sync (#49)
- Preserve DOWNLOADED state after remote file is auto-deleted (#50)
- Fix dashboard status sort: groups ordered logically, oldest-first within each group (#47)
- Config persistence hardened against data loss - Config writes now use atomic file operations (temp file + fsync + rename) to prevent truncation if the process is killed mid-write. Missing config sections fall back to defaults instead of triggering a backup-and-reset cycle. A warning is now logged when config parse failure causes fallback to defaults. (#32)
- Web access logging - Moved all HTTP request access logs from INFO to DEBUG level to reduce log noise during normal operation
- Config file overwritten on container restart - Config files from older versions (missing newer properties like
auto_delete_remoteornet_limit_rate) no longer cause parse errors that trigger a backup-and-overwrite cycle. Config loading is now resilient to missing keys (uses defaults) and unknown keys/sections (silently ignored), enabling smooth upgrades across versions.
- Angular 4 → 21 - Complete frontend rewrite with modern Angular 21
- Standalone components replacing NgModule architecture
- New Angular control flow syntax (@if, @for, @switch)
inject()function replacing constructor dependency injection- Bootstrap 4 → 5.3 with JS bundle for dropdowns
- Font Awesome 4 → 7 (Free)
- RxJS 5 → 7 with modern pipe operators
- Immutable.js removed — plain TypeScript interfaces
- Node 12 → 22 for builds
npm install→npm cifor reproducible builds
- Smaller transfer size - 156 kB gzipped (down from ~300 kB)
- Settings page - Simplified layout with all sections always expanded
- Unit tests - 125 Vitest tests across 14 new spec files covering all models, pipes, and services
- Angular 4 code - Old frontend code (
src/angular-old/,src/angular-v17/) removed - Immutable.js - Replaced with plain TypeScript interfaces
- ngx-modialog - Replaced with inline confirmation patterns
- angular-webstorage-service - Replaced with direct localStorage usage
- css-element-queries - Replaced with native ResizeObserver
- Auto-delete from remote after download - New "Delete from remote after download" option in AutoQueue settings. When enabled, files are automatically deleted from the remote seedbox after successful download, preventing the remote server from filling up. Works correctly with auto-extract — extraction runs before deletion when both are enabled. (#25)
- Delete remote with tilde (~) path - Fixed "Delete remote" command not working when the remote path uses
~. Now properly converts tilde to$HOMEfor shell expansion, matching the fix previously applied to the remote scanner. (#27) - Remote shell auto-detection - SeedSync now automatically detects the available shell on the remote server (
/bin/bash,/usr/bin/bash,/bin/sh) instead of failing when/bin/bashdoesn't exist. Uses SFTP fallback for detection when the login shell is broken. (#18) - SSH key auth no longer requires password - The password field is now optional when SSH key authentication is enabled. Previously required a dummy value. (#21)
- Config serialization of None values - Fixed
Nonevalues being written as the string "None" in config files, which caused issues with new optional fields.
- Bandwidth/speed limit - New "Bandwidth Limit" setting in the Connections section allows capping download speed. Supports values like
500K,2M, or raw bytes/sec. Set to0or leave empty for unlimited. (#24) - SSH key auth documentation - README and docker-compose now document how to mount SSH keys for password-less authentication.
- Shared remote path utilities - Extracted tilde-aware path escaping into a shared utility module for reuse across scanner and delete operations.
- Makefile test target - Fixed
make testto install all required Python dependencies.
- Remote paths with tilde (~) - Fixed remote scanner to properly handle paths containing
~. The tilde is now converted to$HOMEfor shell expansion, allowing users whose SSH and LFTP paths differ (e.g., LFTP locked to home directory). (#14) - LftpJobStatusParser crash on empty output - Fixed parser crashing with "Missing queue header line 1" when lftp
jobs -vreturns empty or unexpected output. Now gracefully returns empty status instead of crashing. (#15) - ANSI escape codes in LFTP output - Fixed parser failing on "First line is not a matching header" when LFTP output contains ANSI escape sequences like bracketed paste mode (
^[[?2004l). These terminal control codes are now stripped before parsing. (#15)
- Restart button - Fixed server restart functionality that was broken due to Bottle framework's custom
__setattr__blocking attribute updates. Now usesthreading.Eventfor thread-safe signaling. - Settings persistence - Settings are now properly saved before restart
- Restart notification - "Restart the app to apply new settings" notification now clears immediately when restart button is clicked
- About page - Updated GitHub repository link to nitrobass24/seedsync and copyright information
- scanfs architecture mismatch - Force AMD64 build for scanfs binary since it runs on remote seedbox servers, not locally. Fixes "cannot execute binary file: Exec format error" on ARM-based local machines.
- Angular 17 rollback - Reverted to Angular 4 due to runtime issues. Angular 17 code preserved in
src/angular-v17/for future work.
- Dockerfile - Reverted to Node 12 for Angular 4 compatibility
- Angular 4 → 17 - Complete frontend rewrite with modern Angular
- Standalone components (no NgModules)
- Bootstrap 4 → 5.3 (removes jQuery dependency)
- RxJS 5 → 7 with pipe operators
- Immutable.js removed (native TypeScript)
- ngx-modialog → Angular CDK Dialog
- Node 12 → Node 20 for builds
- Smaller JS bundles - ~150-200KB reduction from removed dependencies
- scanfs builder - Now uses Debian Bullseye (glibc 2.31) as Buster repos are EOL
src/angular-v4/- Original Angular 4 code preserved for rollback if needed
- Dockerfile compatibility - Fixed Python/Debian version combinations for multi-stage build
- Docker entrypoint UID/GID handling - Fixed container crash when PGID matches an existing group ID in the container (e.g., Synology with GID 101). Now checks for existing UID/GID by ID instead of name, allowing reuse of pre-existing system groups. (#4)
- scanfs glibc compatibility - Fixed "Failed to load Python shared library" error on older seedbox servers by building scanfs on Debian Buster (glibc 2.28) instead of Bookworm (glibc 2.36). Now supports Linux systems from 2018+. (#5)
- Improved entrypoint logging - Now shows whether existing users/groups are being reused for easier debugging of permission issues
- Updated requests to >=2.32.0 (fixes credential leak, session verification issues)
- Updated urllib3 to >=2.2.0 (fixes redirect bombs, decompression vulnerabilities)
- Updated certifi to >=2024.7.4 (removes revoked root certificates)
- Updated pyinstaller to >=6.0 (fixes privilege escalation in build)
- poetry.lock - Removed unused lock file that was triggering 30 Dependabot alerts
- mkdocs/mkdocs-material - Removed documentation tools from pyproject.toml (not needed at runtime)
- Cleaned up
pyproject.tomlwith explicit version constraints
- Python 3.12 regex warnings - Converted all regex patterns to raw strings across:
lftp/job_status_parser.py- LFTP output parsing patternssystem/scanner.py- File size parsing patternscontroller/extract/dispatch.py- RAR file extension pattern
- CI workflow - Updated
softprops/action-gh-releaseto v2 withmake_latest: true
- Docker-only deployment - Removed Debian package support, simplified to Docker-only
- Image size reduced 45% - From 439MB to 240MB
- Python 3.12 - Updated from Python 3.11 to 3.12
- Replaced Poetry with pip - Faster builds, smaller image
- Replaced node-sass with sass - Pure JS, no native compilation needed
- Fixed Python 3.12 regex warnings - Updated to raw strings
- Debian packaging (
src/debian/) - Legacy build files (
src/docker/build/deb/,src/docker/stage/) - mkdocs/mkdocs-material from runtime (documentation tools not needed)
requirements.txt- Minimal runtime dependencies- Simplified
Makefilewith Docker-focused commands
- Docker image now works - Complete rewrite of Dockerfile to properly build all components
- Angular frontend included - Web UI was missing from previous Docker builds
- scanfs binary included - File system scanner now properly built and included
- PUID/PGID permissions - Improved entrypoint script for proper user/group handling
- Python 3.11 support - Updated from Python 3.8 (EOL) to Python 3.11
- TypeScript build errors - Fixed type conflicts in Angular build
- Dockerfile: Complete multi-stage build that includes:
- Stage 1: Angular frontend build (Node 12)
- Stage 2: scanfs binary build (PyInstaller)
- Stage 3: Python runtime with all components
- pyproject.toml: Updated Python version constraint from
~3.8to^3.8(allows 3.8-3.12) - package.json: Updated node-sass version for build compatibility
- tsconfig.json: Added
skipLibCheckto resolve type definition conflicts - entrypoint.sh: Enhanced permission handling for mounted volumes
- docker-compose.dev.yml: Development compose file for easy local testing
- MODERNIZATION_PLAN.md: Documentation of codebase architecture and future roadmap
- Base image:
python:3.12-slim-bookworm - Node version for Angular build: 12.22 (required for Angular 4.x compatibility)
- Uses pip for Python dependency management (faster, smaller)
- Multi-architecture support preserved (amd64, arm64, arm/v7)
- Angular 4.x is outdated but functional; upgrade to modern Angular would require significant work
- Some seedbox providers may have issues with the scanfs binary due to
/tmpmount restrictions (see GitHub issues #97, #136)
See the original repository for earlier changelog entries: https://github.com/ipsingh06/seedsync/releases