SeedSync is a Docker-based tool that syncs files from a remote seedbox to a local machine using LFTP.
- Frontend: Angular 21 (Bootstrap 5.3, Font Awesome 7, Vitest)
- Backend: Python 3.13 with Bottle
- Container: Multi-arch Docker image (amd64, arm64)
- Registry: ghcr.io/nitrobass24/seedsync
src/
├── angular/ # Angular 21 frontend
├── python/ # Python backend
├── docker/ # Docker build files
└── e2e-playwright/ # Playwright end-to-end tests
website/ # Docusaurus documentation site
- master - Stable release branch
- develop - Integration branch for all new work
All work MUST follow this branching discipline:
- Always start from
develop: Before writing any code, checkoutdevelopand pull latest:git checkout develop && git pull origin develop - Create a feature branch: Every task (feature, bugfix, refactor) gets its own branch off
develop:git checkout -b feat/short-description # or fix/short-description - Commit only to the feature branch: Never commit directly to
developormaster. - One concern per branch: Do not mix unrelated changes into the same branch. If you discover a separate issue while working, finish or stash your current work, then create a new branch for the other issue.
- Open a PR to
develop: When the work is complete, push the feature branch and open a PR targetingdevelop. Include a summary and test plan. Releases are the only path that PRs tomaster— see the Release Process below. - Do not carry dirty working-tree changes across branches: Before switching branches, either commit or stash. Never rely on uncommitted edits surviving a
git checkout.
When merging a PR or completing significant work, follow this release process:
Add a new version entry at the top with:
- Version number following semver (major.minor.patch)
- Date in YYYY-MM-DD format
- Sections: Changed, Added, Fixed, Removed, Security (as applicable)
Example:
## [0.10.0] - 2026-01-27
### Changed
- **Feature name** - Description of change
### Fixed
- **Bug name** - Description of fixUpdate the version in src/angular/package.json to match the release version. This is displayed on the About page.
Releases use a release/vX.Y.Z branch off develop, a PR into master, and a tag on the resulting merge commit. Never commit the release directly to develop or master.
# Branch from the latest develop
git checkout develop && git pull origin develop
git checkout -b release/vX.Y.Z
# Stage the release commit (CHANGELOG / package.json)
git add CHANGELOG.md src/angular/package.json
git commit -m "Release vX.Y.Z - Brief description"
git push -u origin release/vX.Y.Z
# Open a PR targeting master
gh pr create --base master --head release/vX.Y.Z \
--title "Release vX.Y.Z - Brief description" \
--body "Sync develop → master for vX.Y.Z. See CHANGELOG.md for details."
# After the PR is merged into master, fetch and tag the merge commit
git checkout master && git pull origin master
git tag vX.Y.Z
git push origin vX.Y.ZThe CI workflow will automatically:
- Build multi-arch Docker images
- Push to ghcr.io
- Create GitHub release with auto-generated notes
After CI creates the release, update the release notes with detailed changelog:
gh release edit vX.Y.Z --repo nitrobass24/seedsync --notes "$(cat <<'EOF'
## What's Changed
### Fixed
- **Bug name** - Description of fix (#issue)
### Changed
- **Feature name** - Description of change
### Added
- **New feature** - Description
## Docker Pull
```bash
docker pull ghcr.io/nitrobass24/seedsync:X.Y.ZFull Changelog: https://github.com/nitrobass24/seedsync/compare/vPREV...vX.Y.Z EOF )"
Format should match CHANGELOG.md entries with:
- Section headers: Fixed, Changed, Added, Removed, Security
- Bold feature/bug names
- Issue references where applicable
- Always include a "Docker Pull" section with the `docker pull` command for the release version
### 5. Verify Release
- Check GitHub Actions completed successfully
- Verify image is available: `docker pull ghcr.io/nitrobass24/seedsync:X.Y.Z`
- Verify GitHub release notes are formatted correctly
## Version Numbering
- **Major** (X.0.0): Breaking changes, major rewrites
- **Minor** (0.X.0): New features, significant improvements (e.g., Angular upgrade)
- **Patch** (0.0.X): Bug fixes, minor updates
## Key Files
| File | Purpose |
|------|---------|
| `CHANGELOG.md` | Release history and notes |
| `src/docker/build/docker-image/Dockerfile` | Multi-stage Docker build |
| `.github/workflows/ci.yml` | CI/CD pipeline |
| `.github/workflows/docs-pages.yml` | Documentation deployment |
| `website/` | Documentation site (Docusaurus) |
## CI/CD Workflows
### CI (`ci.yml`)
Triggers: `push` to `master` or `develop`, `pull_request` against `master` or `develop`, and `push` of a tag matching `v[0-9]+.[0-9]+.[0-9]+`.
Release-relevant matrix:
| Trigger | Build & Test | Publish Image | Create Release |
|---------|--------------|---------------|----------------|
| PR to master | ✅ | ❌ | ❌ |
| Push to master | ✅ | ❌ | ❌ |
| Push tag (`v*.*.*`) | ✅ | ✅ | ✅ |
- **Build & Test**: Builds Docker image and verifies container starts
- **Publish Image**: Pushes multi-arch image to ghcr.io (only on release tags)
- **Create Release**: Creates GitHub release with auto-generated notes (only on release tags)
### Docs (`docs-pages.yml`)
- Triggers only when `website/` directory changes
- Builds and deploys Docusaurus site to GitHub Pages
## Common Tasks
### Building Locally
```bash
make build # Build Docker image
make run # Run container
make logs # View logs
make stop # Stop container
cd src/angular && npx ng lint # Angular ESLint
cd src/angular && npx ng test # Angular Vitest unit tests
# Python tests run in Docker via CI (pytest in test-image)
# CI also runs: Docker build, container startup, web UI accessibilityEvery PR MUST include appropriate test changes. Tests are not optional — they ship with the code.
- Add unit tests for all new logic (services, components, utilities, handlers)
- Cover the happy path, edge cases, and error/validation paths
- If adding a new API endpoint, add integration tests using
WebTest/TestApp
- Add a test that reproduces the bug (fails before fix, passes after)
- If the bug was in untested code, add baseline tests for the surrounding logic
- Existing tests must continue to pass without modification (unless the refactor intentionally changes behavior)
- If refactoring reveals untested code, add tests for it
- Remove or update tests that cover the deleted code
- Do not leave dead test code behind
| Layer | Framework | Location | Runner |
|---|---|---|---|
| Angular lint | ESLint (angular-eslint) | src/angular/eslint.config.js |
cd src/angular && npx ng lint |
| Angular unit tests | Vitest | src/angular/src/**/*.spec.ts |
cd src/angular && npx ng test |
| Python unit tests | unittest | src/python/tests/unittests/ |
pytest (in Docker test-image) |
| Python integration tests | unittest + WebTest | src/python/tests/integration/ |
pytest (in Docker test-image) |
- Angular: Use
describe/itfrom Vitest. Mock services withvi.fn(). UseTestBedfor component tests. Query DOM by text content or stable attributes, not positional selectors (nth-of-type). - Python: Use
unittest.TestCase. Useunittest.mock.MagicMock/patchfor mocking. Integration tests for web handlers usewebtest.TestAppwithBaseTestWebApp. - Naming: Test files mirror source files —
foo.service.ts→foo.service.spec.ts,controller.py→test_controller.py. - No flaky tests: Avoid
time.sleepin tests where possible. Usethreading.Eventor barriers for synchronization. Usetimeout_decoratorfor tests that might hang.
These are guidelines, not hard caps. Their real job is to slow you down and ask "is this still one concern?" Cross them when the code is clearer for it — flag in the PR body when you do.
| Unit | Target | When to pause |
|---|---|---|
| File | ≤ 500 lines | At 400, ask "one concern or several?" |
| Class | ≤ 300 lines | If state covers > 2 concerns, extract |
| Function / method | ≤ 40 lines | Longer is OK when linear; deep nesting is not |
| Cyclomatic complexity (per function) | ≤ 12 | Enforced in CI via ruff C901 (max-complexity = 12) |
Cyclomatic complexity is the one number that actually predicts pain — a 200-line linear function is fine; a 40-line function with 5 nested conditionals is dangerous. Use ruff check --select C901 locally to measure.
Before adding a method to an existing class, ask:
- Does this belong in a sibling module/class instead?
- Does the class already span multiple concerns (auth + persistence + validation, etc.)? If so, extracting first is usually worth the overhead.
- Prefer composition over accretion — reach for a new collaborator, not a bigger class.
- A focused unit test needs
cls.__new__(cls)or broad mocking just to exercise one method. - The constructor initializes unrelated state (e.g., both a model and a command queue and a logger facade).
- One method dominates the file's line count.
- Changes in separate features keep conflicting in the same file.
When you see these, open an issue for the extraction rather than working around them silently.
- Qualitative checks: code review. Reviewers should push back on "this file is getting unwieldy" even without a hard rule.
- Mechanical checks (Python): ruff's
C901(cyclomatic complexity) withmax-complexity = 12. Enforced in CI; existing outliers are annotated with# noqa: C901. Test files (tests/**) are excluded viaper-file-ignoresinpyproject.toml. - Ratchet pattern: when a bound exists but the codebase has outliers, set the threshold just above the worst and drop it over time (same pattern we use for
--max-warningsinng lint).
Services that mutate a BehaviorSubject from a REST/HTTP call should follow one documented contract so callers can reason about side effects:
- Update the subject inside the returned observable's pipeline (
tap/map), not via a second internal.subscribe(). A second internal subscribe fires the side effect independently of the caller, runs the request a second time when the source is cold, and makes "when does the store update?" caller-independent and surprising. Folding the mutation into the returned pipeline means the store updates exactly when (and as many times as) the caller subscribes. - Return a typed result and recover errors with
catchErrorso the caller always gets a value (or a typed failure) rather than an error notification. Map HTTP responses to a small typed result/WebReaction.
IntegrationsService (src/angular/src/app/services/settings/integrations.service.ts) is the reference implementation: create/update/remove/test use tap/map to update instancesSubject and catchError to map failures to a typed result, with no second subscribe.
ConfigService.set and AutoQueueService.add/remove were migrated to this contract (#542): they now fold their BehaviorSubject mutation into the returned pipeline via tap (no second internal subscribe) and return the typed WebReaction (RestService.sendRequest already recovers HTTP errors into a failure WebReaction, so no extra catchError is needed). Their specs subscribe to the returned observable before asserting the subject mutated, since the mutation now defers to caller subscription. No remaining services use the legacy second-subscribe pattern.
- Repo: github.com/nitrobass24/seedsync
- Docs: nitrobass24.github.io/seedsync
- Issues: Use for bug reports and feature requests