feat(account): auto-select best account per service
Summary
Implements AccountSelector application service to rank multiple accounts for the same hoster/debrid service deterministically. Supports three configurable strategies (BestTraffic, RoundRobin, Manual) read from live config. Unblocks task 25 (rotation auto sur quota).
Why
Task 24 from PRD §6.4: plugins need deterministic account dispatch to handle quota rotation and fallback chains. BestTraffic ranks by traffic availability with tie-breakers for reproducibility. RoundRobin distributes load across eligible accounts. Manual reserved for future pinning UI.
Changes
-
Add
AccountSelectorservice insrc-tauri/src/application/services/account_selector.rs(~685 lines)- Three strategies:
BestTraffic(enabled → non-expired → most traffic → most recent validation → smallest id),RoundRobin(per-service cursor),Manual(alias of BestTraffic + regression test) - Per-service candidate cache invalidated on account-touching events
- Domain events:
NoAccountAvailable { service_name }andAccountSelected { id, service_name, strategy }
- Three strategies:
-
Extend domain models
AccountSelectionStrategyenum indomain/model/account.rswith Display/FromStr (snake_case serialization)account_selection_strategyfield onAppConfigandConfigPatchwithapply_patch()support
-
Update adapters
- Tauri bridge: serialize
no-account-availableandaccount-selectedevents to frontend - TOML store: persist strategy to
config.toml(snake_case:best_traffic,round_robin,manual) - IPC DTOs: add
account_selection_strategyfield (camelCase) for settings/patch endpoints
- Tauri bridge: serialize
-
Extend CommandBus with
account_selector()accessor andresolve_account_for(service_name)convenience method -
Flag planned hook in
resolve_links.rs(comment only; actual per-URL calls deferred to avoid emittingAccountSelectedonce per probed link) -
Update CHANGELOG.md with full feature description
Testing
✅ All 12 unit tests pass covering four PRD acceptance criteria:
- 3-account scenario (full-traffic account wins over expired/low-traffic)
- All-expired returns
None+NoAccountAvailableevent - Comparative ranking table (enabled → non-expired → traffic → validation → id)
- RoundRobin alternance with cursor persistence across cache invalidation
✅ Comprehensive edge cases: cache invalidation, case-mismatch behavior, tie-breaker correctness, config strategy reading
✅ Integration: config patch apply, event emission, Tauri IPC serialization
Command output:
cargo test --package vortex-app --lib account_selector
# 12 passed
cargo clippy --workspace -- -D warnings
# clean
cargo fmt --check
# ok
Related Issues
- Refs: PRD §6.4 (Auto-select meilleur compte)
- Refs: PRD-v2 §P1.5 (task 24)
- Unblocks: task 25 (rotation auto sur quota)
Notes for Reviewer
- Traffic ranking:
Unlimitedenum variant ranks above anyFinite(u64)via Ord derivation, ensuring unlimited plans always win - Cache survival: per-service cursors in RoundRobin survive bulk cache invalidation so hot-reload of account set does not reset rotation
- Weak<Mutex> pattern prevents Arc cycle between EventBus subscriber and AccountSelector while allowing events to invalidate cache
- Config override:
resolve_account_for()reads live strategy from AppConfig each call, honoring runtime changes without restart - Deterministic tie-breaking: enabled → not expired → traffic_left → last_validated_at → id ensures reproducible selection for same candidate set
Checklist
- Tests added and passing (12 unit tests, 4 acceptance criteria verified)
- Docs updated (CHANGELOG.md, inline doc comments, task frontmatter)
- No secrets, debug prints, or allow-directives
- Self-reviewed diff
- Cargo clippy + fmt clean
Summary by cubic
Auto-select the best account per service with a configurable strategy. Deterministic selection from live repo state, a single entry point for plugins/downloads, and backward-compatible config loading; implements PRD §6.4 (Linear task 24).
-
New Features
- Added
AccountSelectorwith three strategies:BestTraffic(default),RoundRobin(per-service cursor), andManual(alias ofBestTrafficfor now). - New events:
NoAccountAvailableandAccountSelected; forwarded via Tauri asno-account-availableandaccount-selected. CommandBus::resolve_account_for(service_name)uses the liveAppConfigstrategy.- Config:
account_selection_strategyonAppConfig/ConfigPatch, persisted to TOML and exposed via IPC ("best_traffic" | "round_robin" | "manual").
- Added
-
Bug Fixes
- Removed selector caching to fix a race that could serve stale rows.
RoundRobin: a poisoned cursor now surfaces as a validation error instead of returningNone.- Strict validation of
account_selection_strategyin IPC and TOML;settings_updatesurfaces errors. Legacyconfig.tomlwithout the field now loads with the default (BestTraffic), while unknown non-empty values fail fast. resolve_account_forpropagatesConfigStore::get_config()failures; tests cover repo-fresh selection, case sensitivity, validation, poisoned-cursor handling, and legacy-config compatibility.
Summary by CodeRabbit
-
New Features
- Configurable account selection strategies in Settings: Best Traffic, Round Robin, Manual.
- Account resolution API for operations needing credentials.
- Accounts management UI with add/update/delete/validate/import/export and persisted account storage.
-
Bug Fixes
- Invalid/unknown strategy values are rejected and surface as errors (no silent fallback).
-
Behavior
- Account lifecycle events forwarded to the frontend: account selected, no account available.