* fix: --host filter now limits multi-host stack operations to single host
Previously, when using `-H host` with a multi-host stack like `glances: all`,
the command would find the stack (correct) but then operate on ALL hosts for
that stack (incorrect). For example, `cf down -H nas` with `glances` would
stop glances on all 5 hosts instead of just nas.
Now, when `--host` is specified:
- `cf down -H nas` only stops stacks on nas, including only the nas instance
of multi-host stacks
- `cf up -H nas` only starts stacks on nas (skips migration logic since
host is explicitly specified)
Added tests for the new filter_host behavior in both executor and CLI.
* fix: Apply filter_host to logs and ps commands as well
Same bug as up/down: when using `-H host` with multi-host stacks,
logs and ps would show results from all hosts instead of just the
filtered host.
* fix: Don't remove multi-host stacks from state when host-filtered
When using `-H host` with a multi-host stack, we only stop one instance.
The stack is still running on other hosts, so we shouldn't remove it
from state entirely.
This prevents issues where:
- `cf apply` would try to re-start the stack
- `cf ps` would show incorrect running status
- Orphan detection would be confused
Added tests to verify state is preserved for host-filtered multi-host
operations and removed for full stack operations.
* refactor: Introduce StackSelection dataclass for cleaner context passing
Instead of passing filter_host separately through multiple layers,
bundle the selection context into a StackSelection dataclass:
- stacks: list of selected stack names
- config: the loaded Config
- host_filter: optional host filter from -H flag
This provides:
1. Cleaner APIs - context travels together instead of being scattered
2. is_instance_level() method - encapsulates the check for whether
this is an instance-level operation (host-filtered multi-host stack)
3. Future extensibility - can add more context (dry_run, verbose, etc.)
Updated all callers of get_stacks() to use the new return type.
* Revert "refactor: Introduce StackSelection dataclass for cleaner context passing"
This reverts commit e6e9eed93e.
* feat: Proper per-host state tracking for multi-host stacks
- Add `remove_stack_host()` to remove a single host from a multi-host stack's state
- Add `add_stack_host()` to add a single host to a stack's state
- Update `down` command to use `remove_stack_host` for host-filtered multi-host stacks
- Update `up` command to use `add_stack_host` for host-filtered operations
This ensures the state file accurately reflects which hosts each stack is running on,
rather than just tracking if it's running at all.
* fix: Use set comparisons for host list tests
Host lists may be reordered during YAML save/load, so test for
set equality rather than list equality.
* refactor: Merge remove_stack_host into remove_stack as optional parameter
Instead of a separate function, `remove_stack` now takes an optional
`host` parameter. When specified, it removes only that host from
multi-host stacks. This reduces API surface and follows the existing
pattern.
* fix: Restore deterministic host list sorting and add filter_host test
- Restore sorting of list values in _sorted_dict for consistent YAML output
- Add test for logs --host passing filter_host to run_on_stacks
* Sort host lists in state file for consistent output
Multi-host stacks (like glances) now have their host lists sorted
alphabetically when saving state. This makes git diffs cleaner by
avoiding spurious reordering changes.
* config: Add local_host and web_stack options
Allow configuring local_host and web_stack in compose-farm.yaml instead
of requiring environment variables. This makes it easier to deploy the
web UI with just a config file mount.
- local_host: specifies which host is "local" for Glances connectivity
- web_stack: identifies the web UI stack for self-update detection
Environment variables (CF_LOCAL_HOST, CF_WEB_STACK) still work as
fallback for backwards compatibility.
Closes#152
* docs: Clarify glances_stack is used by CLI and web UI
* config: Env vars override config, add docs
- Change precedence: environment variables now override config values
(follows 12-factor app pattern)
- Document all CF_* environment variables in configuration.md
- Update example-config.yaml to mention env var overrides
* config: Consolidate env vars, prefer config options
- Update docker-compose.yml to comment out CF_WEB_STACK and CF_LOCAL_HOST
(now prefer setting in compose-farm.yaml)
- Update init-env to comment out CF_LOCAL_HOST (can be set in config)
- Update docker-deployment.md with new "Config option" column
- Simplify troubleshooting to prefer config over env vars
* config: Generate CF_LOCAL_HOST with config alternative note
Instead of commenting out CF_LOCAL_HOST, generate it normally but add
a note in the comment that it can also be set as 'local_host' in config.
* config: Extend local_host to all web UI operations
When running the web UI in a Docker container, is_local() can't detect
which host the container is on due to different network namespaces.
Previously local_host/CF_LOCAL_HOST only affected Glances connectivity.
Now it also affects:
- Container exec/shell (runs locally instead of via SSH)
- File editing (uses local filesystem instead of SSH)
Added is_local_host() helper that checks CF_LOCAL_HOST/config.local_host
first, then falls back to is_local() detection.
* refactor: DRY get_web_stack helper, add tests
- Move get_web_stack to deps.py to avoid duplication in streaming.py
and actions.py
- Add tests for config.local_host and config.web_stack parsing
- Add tests for is_local_host, get_web_stack, and get_local_host helpers
- Tests verify env var precedence over config values
* glances: rely on CF_WEB_STACK for container mode
Restore docker-compose env defaults and document local_host scope.
* web: ignore local_host outside container
Document container-only behavior and adjust tests.
* web: infer local host from web_stack
Drop local_host config option and update docs/tests.
* Remove CF_LOCAL_HOST override
* refactor: move web_stack helpers to Config class
- Add get_web_stack() and get_local_host_from_web_stack() as Config methods
- Remove duplicate _get_local_host_from_web_stack() from glances.py and deps.py
- Update deps.py get_web_stack() to delegate to Config method
- Add comprehensive tests for the new Config methods
* config: remove web_stack config option
The web_stack config option was redundant since:
- In Docker, CF_WEB_STACK env var is always set
- Outside Docker, the container-specific behavior is disabled anyway
Simplify by only using the CF_WEB_STACK environment variable.
* refactor: remove get_web_stack wrapper from deps
Callers now use config.get_web_stack() directly instead of
going through a pointless wrapper function.
* prompts: add rule to identify pointless wrapper functions
* fix: Ignore _version.py in type checkers
The _version.py file is generated at build time by hatchling,
so mypy and ty can't resolve it during development.
* Update README.md
* cli: Respect --host flag in stats summary and add tests
- Fix --host filter to work in non-containers mode (was ignored)
- Filter hosts table, pending migrations, and --live queries by host
- Add tests for stats --containers functionality
* refactor: Remove redundant _format_bytes wrappers
Use format_bytes directly from glances module instead of wrapper
functions that add no value.
* Fix stats --host filtering
* refactor: Move validate_hosts to top-level imports
Previously `cf config init-env` created the .env file next to the
compose-farm.yaml config file. This was unintuitive when working in
stack subdirectories - users expected the file in their current
directory.
Now the default is to create .env in the current working directory,
which matches typical CLI tool behavior. Use `-o /path/to/.env` to
specify a different location.
* fix: external network name parsing
Compose network definitions may have a "name" field defining the actual network name,
which may differ from the key used in the compose file e.g. when overriding the default
compose network, or using a network name containing special characters that are not valid YAML keys.
Fix: check for "name" field on definition and use that if present, else fall back to key.
* tests: Add test for external network name field parsing
Covers the case where a network definition has a "name" field that
differs from the YAML key (e.g., default key with name: compose-net).
---------
Co-authored-by: Bas Nijholt <bas@nijho.lt>
* up: Add --pull and --build flags for Docker Compose parity
Add `--pull` and `--build` options to `cf up` to match Docker Compose
naming conventions. This allows users to pull images or rebuild before
starting without using the separate `update` command.
- `cf up --pull` adds `--pull always` to the compose command
- `cf up --build` adds `--build` to the compose command
- Both flags work together: `cf up --pull --build`
The `update` command remains unchanged as a convenience wrapper.
* Update README.md
* up: Run stacks in parallel when no migration needed
Refactor up_stacks to categorize stacks and run them appropriately:
- Simple stacks (no migration): run in parallel via asyncio.gather
- Multi-host stacks: run in parallel
- Migration stacks: run sequentially for clear output and rollback
This makes `cf up --all` as fast as `cf update --all` for typical use.
* refactor: DRY up command building with build_up_cmd helper
Consolidate all 'up -d' command construction into a single helper
function. Now used by up, update, and operations module.
Added tests for the helper function.
* update: Delegate to up --pull --build
Simplify update command to just call up with pull=True and build=True.
This removes duplication and ensures consistent behavior.
* update: Only restart containers when images change
Use `up -d --pull always --build` instead of separate pull/build/down/up
steps. This avoids unnecessary container restarts when images haven't
changed.
* Update README.md
* docs: Update update command description across all docs
Reflect new behavior: only recreates containers if images changed.
* Update README.md
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Previously, Pydantic validation errors like "Extra inputs are not
permitted" didn't show which field caused the error. Now the error
message includes the field location (e.g., "unknown_key: Extra inputs
are not permitted").
* perf: Optimize stray detection to use 1 SSH call per host
Previously, stray detection checked each stack on each host individually,
resulting in (stacks * hosts) SSH calls. For 50 stacks across 4 hosts,
this meant ~200 parallel SSH connections, causing "Connection lost" errors.
Now queries each host once for all running compose projects using:
docker ps --format '{{.Label "com.docker.compose.project"}}' | sort -u
This reduces SSH calls from ~200 to just 4 (one per host).
Changes:
- Add get_running_stacks_on_host() in executor.py
- Add discover_all_stacks_on_all_hosts() in operations.py
- Update _discover_stacks_full() to use the batch approach
* Remove unused function and add tests
- Remove discover_stack_on_all_hosts() which is no longer used
- Add tests for get_running_stacks_on_host()
- Add tests for discover_all_stacks_on_all_hosts()
- Verifies it returns correct StackDiscoveryResult
- Verifies stray detection works
- Verifies it makes only 1 call per host (not per stack)
* Add self-healing: detect and stop rogue containers
Adds the ability to detect and stop "rogue" containers - stacks running
on hosts they shouldn't be according to config.
Changes:
- `cf refresh`: Now scans ALL hosts and warns about rogues/duplicates
- `cf apply`: Stops rogue containers before migrations (new phase)
- New `--no-rogues` flag to skip rogue detection
Implementation:
- Add StackDiscoveryResult for full host scanning results
- Add discover_stack_on_all_hosts() to check all hosts in parallel
- Add stop_rogue_stacks() to stop containers on unauthorized hosts
- Update tests to include new no_rogues parameter
* Update README.md
* fix: Update refresh tests for _discover_stacks_full return type
The function now returns a tuple (discovered, rogues, duplicates)
for rogue/duplicate detection. Update test mocks accordingly.
* Rename "rogue" terminology to "stray" for consistency
Terminology update across the codebase:
- rogue_hosts -> stray_hosts
- is_rogue -> is_stray
- stop_rogue_stacks -> stop_stray_stacks
- _discover_rogues -> _discover_strays
- --no-rogues -> --no-strays
- _report_rogue_stacks -> _report_stray_stacks
"Stray" better complements "orphaned" (both evoke lost things)
while clearly indicating the stack is running somewhere it
shouldn't be.
* Update README.md
* Move asyncio import to top level
* Fix remaining rogue -> stray in docstrings and README
* Refactor: Extract shared helpers to reduce duplication
1. Extract _stop_stacks_on_hosts helper in operations.py
- Shared by stop_orphaned_stacks and stop_stray_stacks
- Reduces ~50 lines of duplicated code
2. Refactor _discover_strays to reuse _discover_stacks_full
- Removes duplicate discovery logic from lifecycle.py
- Calls management._discover_stacks_full and merges duplicates
* Add PR review prompt
* Fix typos in PR review prompt
* Move import to top level (no in-function imports)
* Update README.md
* Remove obvious comments
* refactor(web): store backups in XDG config directory
Move file backups from `.backups/` alongside the file to
`~/.config/compose-farm/backups/` (respecting XDG_CONFIG_HOME).
The original file path is mirrored inside to avoid name collisions.
* docs(web): document automatic backup location
* refactor(paths): extract shared config_dir() function
* fix(web): use path anchor for Windows compatibility
* feat(web): add Open Website button and command for stacks with Traefik labels
Parse traefik.http.routers.*.rule labels to extract Host() rules and
display "Open Website" button(s) on stack pages. Also adds the command
to the command palette.
- Add extract_website_urls() function to compose.py
- Determine scheme (http/https) from entrypoint (websecure/web)
- Prefer HTTPS when same host has both protocols
- Support environment variable interpolation
- Add external_link icon from Lucide
- Add comprehensive tests for URL extraction
* refactor: move extract_website_urls to traefik.py and reuse existing parsing
Instead of duplicating the Traefik label parsing logic in compose.py,
reuse generate_traefik_config() with check_all=True to get the parsed
router configuration, then extract Host() rules from it.
- Move extract_website_urls from compose.py to traefik.py
- Reuse generate_traefik_config for label parsing
- Move tests from test_compose.py to test_traefik.py
- Update import in pages.py
* test: add comprehensive tests for extract_website_urls
Cover real-world patterns found in stacks:
- Multiple Host() in one rule with || operator
- Host() combined with PathPrefix (e.g., && PathPrefix(`/api`))
- Multiple services in one stack (like arr stack)
- Labels in list format (- key=value)
- No entrypoints (defaults to http)
- Multiple entrypoints including websecure
* fix: Skip buildable images in pull command
Add --ignore-buildable flag to pull command, matching the behavior
of the update command. This prevents pull from failing when a stack
contains services with local build directives (no remote image).
* test: Fix flaky command palette close detection
Use state="hidden" instead of :not([open]) selector when waiting
for the command palette to close. The old approach failed because
wait_for_selector defaults to waiting for visibility, but a closed
<dialog> element is hidden by design.
- Add "Shell: {service}" commands to the command palette when on a stack page
- Allows quick shell access to containers via `Cmd+K` → type "shell" → select service
- Add `get_container_name()` helper in `compose.py` for consistent container name resolution (used by both api.py and pages.py)
- Enable `-n auto` for all test commands in justfile (parallel execution)
- Add redis stack to test fixtures (missing stack was causing test failure)
- Replace hardcoded timeouts with constants: `TIMEOUT` (10s) and `SHORT_TIMEOUT` (5s)
- Rename `test-unit` → `test-cli` and `test-browser` → `test-web`
- Skip CLI startup test when running in parallel mode (`-n auto`)
- Update test assertions for 5 stacks (was 4)
- Add service-level commands to the command palette when viewing a stack detail page
- Services are extracted from the compose file and exposed via a `data-services` attribute
- Commands are grouped by action (all Logs together, all Pull together, etc.) with services sorted alphabetically
- Service commands appear with a teal indicator to distinguish from stack-level commands (green)
- Implement word-boundary fuzzy matching for better filtering UX:
- `rest plex` matches `Restart: plex-server`
- `server` matches `plex-server` (hyphenated names split into words)
- Query words must match the START of command words (prevents false positives like `r ba` matching `Logs: bazarr`)
Available service commands:
- `Restart: <service>` - Restart a specific service
- `Pull: <service>` - Pull image for a service
- `Logs: <service>` - View logs for a service
- `Stop: <service>` - Stop a service
- `Up: <service>` - Start a service
Add Astral's ty type checker (written in Rust, 10-100x faster than mypy)
as a second type checking layer. Both run in pre-commit and CI.
Fixed type issues caught by ty:
- config.py: explicit Host constructor to avoid dict unpacking issues
- executor.py: wrap subprocess.run in closure for asyncio.to_thread
- api.py: use getattr for Jinja TemplateModule macro access
- test files: fix playwright driver_path tuple handling, pytest rootpath typing
One-shot containers (like CLI tools) were showing a perpetual loading
spinner because they weren't in `docker compose ps` output. Now we:
- Use `ps -a` to include stopped/exited containers
- Display exit code: neutral badge for clean exit (0), error badge for failures
- Show "created" state for containers that were never started
- Create paths.py module with lightweight path utilities (no pydantic)
- Move Config imports to TYPE_CHECKING blocks in CLI modules
- Lazy import load_config only when needed
Combined with asyncssh lazy loading, cf --help now starts in ~150ms
instead of ~500ms (70% faster).
When --full is passed, apply also runs 'docker compose up' on all
services (not just missing/migrating ones) to pick up any config
changes (compose file, .env, etc).
- cf apply # Fast: state reconciliation only
- cf apply --full # Thorough: also refresh all running services