Files
manage/docs/superpowers/specs/2026-07-14-scheduled-actions-design.md
T
2026-07-14 15:22:34 +00:00

11 KiB
Raw Blame History

Typed Scheduled Actions and qBittorrent Polling Design

Date: 2026-07-14 Status: Proposed

Overview

Manage currently collects qBittorrent speed samples as a side effect of a browser polling the widget-data endpoint. This design moves collection into a backend-owned typed scheduler so samples continue when no page is open, while keeping widget reads read-only.

The first scheduled action is qBittorrent speed polling. The scheduler is intentionally extensible but does not execute arbitrary widgets, SSH commands, or user-provided code.

Goals

  • Collect qBittorrent download/upload speed independently of browser presence.
  • Configure polling per qBittorrent service instance.
  • Preserve per-service SQLite isolation and existing cascade-delete behavior.
  • Provide current status, stale-data state, run history, and a manual Run now action.
  • Reuse the existing lifespan worker pattern and remain safe under the single-backend-worker deployment model.
  • Expose metrics and structured logs without adding a new notification channel.

Non-goals

  • Distributed scheduling across replicas.
  • External worker infrastructure or a task queue.
  • Scheduling arbitrary saved SSH tasks.
  • Moving every widget-backed integration to the scheduler in this release.
  • A global scheduler administration page.

Decisions

Area Decision
Architecture Typed action registry with qBittorrent as the first action
Worker model One lifespan-managed backend worker; deployment must run one scheduler-capable backend process
Activation Enabled qBittorrent services poll by default; polling is opt-out
Defaults 15-second interval; valid range 5300 seconds
Samples 30-minute default history; valid range 124 hours; configurable lower sample cap with a hard maximum of 1,200 rows
Timing Immediate first run with per-service startup staggering; fixed delay after completion
Concurrency No overlapping runs and no queued missed ticks per service
Failure Keep enabled, record failure, retry with bounded exponential backoff
Manual run Supported; successful manual run clears backoff
Widget data Read-only; scheduler is the only qBittorrent sampler
Run history Dedicated generic scheduled-action run records; retain 30 days or 1,000 runs per service/action
Service lifecycle Disable pauses and retains history; delete purges service-owned data through cascade deletion
UI Controls and status/history live with each qBittorrent service
Alerts UI and metrics only in this release

Current and target flow

Current

React Query interval
  -> GET /api/widgets/instances/{id}/data
  -> QbittorrentWidgetSource.fetch()
  -> qBittorrent API
  -> append speed sample
  -> return chart data

Target

Backend lifespan
  -> TypedScheduler
  -> registered QbittorrentSpeedAction
  -> qBittorrent API
  -> QbittorrentSampleStore
  -> SchedulerRunStore

React Query / service UI
  -> scheduler status/history/sample endpoints
  -> read-only SQLite queries

Configuration model

The existing qBittorrent service config gains validated non-secret fields:

{
  "base_url": "https://qbit.example",
  "timeout_seconds": 60,
  "polling_enabled": true,
  "poll_interval_seconds": 15,
  "sample_retention_seconds": 1800,
  "sample_max_rows": 1200
}

Suggested validation:

  • polling_enabled: boolean, default true for backward compatibility.
  • poll_interval_seconds: integer from 5 through 300, default 15.
  • sample_retention_seconds: integer from 60 through 86,400, default 1,800.
  • sample_max_rows: integer from 60 through 1,200, default 1,200. The upper bound is a server safety limit, not merely a UI hint.
  • Secrets remain exclusively in the existing encrypted secret fields.

The service type metadata must expose these fields so the existing schema-driven service editor renders them. Existing qBittorrent records receive defaults through normalization rather than a destructive migration.

Scheduler architecture

Registry and contracts

Add a small scheduler service with explicit action registration:

class ScheduledAction(Protocol):
    action_key: str
    async def run(self, service: ServiceRecord, context: ActionContext) -> ActionResult: ...

The registry maps an action key to its implementation and metadata. The first key is qbittorrent.speed_sample. The scheduler never evaluates arbitrary config as executable code.

A scheduler cycle should:

  1. Read enabled service records.
  2. Select services whose typed action is enabled.
  3. Reconcile changed interval/enabled settings.
  4. Run due actions serially per service.
  5. Persist a run record and update in-memory status.
  6. Wait using a stop event so shutdown is responsive.

A thread-based coordinator is appropriate for the first release because BackupAlertPoller already establishes the projects lifespan-managed worker pattern and qBittorrents client is blocking. The action may use the existing authenticated client cache, but the sampler should be extracted from the widget adapter so collection and presentation are not coupled.

Timing and backoff

  • First eligible service run starts immediately after startup, with a small deterministic stagger based on service ordering.
  • Normal scheduling uses fixed delay: the next due time is calculated after the previous attempt completes.
  • A per-service action lock prevents overlap.
  • A failed run uses bounded exponential backoff, capped below the configured intervals operational maximum. Backoff must not enqueue missed runs.
  • A successful scheduled or manual run resets consecutive failures and clears backoff_until.
  • Config changes are observed during the next reconciliation cycle; an interval change affects the next due calculation.
  • Disabling a service prevents new work and allows the current run to finish before the action becomes idle.

Single-worker constraint

The initial design assumes one backend process owns scheduler execution. Running multiple Uvicorn workers or replicas would duplicate polls and run records. Startup logs and operational documentation must make this constraint explicit. A future distributed lease can be added without changing the action contract.

Storage

Speed samples

Extend QbittorrentSampleStore to prune by both:

  • service_id and ts >= now - sample_retention_seconds;
  • most recent sample_max_rows, bounded by 1,200.

The existing qbittorrent_speed_samples table remains the source for chart data. Its API should accept a requested display window and return ordered samples. Deleting a service must continue to cascade into this concern.

Scheduled-action runs

Add a generic scheduler storage concern, separate from service_task_runs, with fields equivalent to:

Field Description
id Run identifier
service_id Owning service instance
action_key Registered action key, e.g. qbittorrent.speed_sample
trigger schedule or manual
started_at / finished_at Attempt timing
status running, success, failure, backoff, or cancelled
duration_ms Elapsed time
attempt Retry/backoff attempt number
error Secret-safe error text, truncated
created_at Record creation time

Indexes should cover (service_id, action_key, started_at DESC) and (status, started_at DESC). Prune records older than 30 days and enforce a maximum of 1,000 records per service/action.

No credentials, request headers, or raw qBittorrent payloads may be stored in run history.

Backend API

Add a dedicated scheduler router. Exact response models should be typed and should not expose secrets.

Endpoint Purpose
GET /api/scheduler/services/{service_id}/status Current action state, last attempt/success, stale state, failures, backoff, and effective config
GET /api/scheduler/services/{service_id}/runs Paginated run history with status/trigger filters
POST /api/scheduler/services/{service_id}/run Run the registered qBittorrent action immediately; return a run/status response
GET /api/scheduler/services/{service_id}/samples Read-only speed samples for a selected display window

The existing PUT /api/services/instances/{id} remains the write path for schedule configuration. The widget-data endpoint must stop calling qBittorrent for the speed kind; it should read samples through the same store/query helper used by the scheduler API.

Manual runs must use the same action registry and persistence path as scheduled runs. A successful manual run clears backoff; a failed manual run records the failure and applies the same bounded retry state.

Stale-data semantics

The status response should include last_success_at, last_error, consecutive_failures, backoff_until, and is_stale. Suggested initial stale rule:

is_stale = no successful run
           OR now - last_success_at > max(2 * effective_interval, 60 seconds)

The speed widget should retain and render the last known samples with a warning containing the last-success time and current error. It should not replace useful history with an empty state solely because the latest poll failed.

Frontend design

The existing schema-driven qBittorrent service editor should gain a scheduling section containing:

  • polling enabled switch;
  • interval field with bounds/error text;
  • sample retention duration;
  • maximum sample rows;
  • effective next-run and last-success summary;
  • Run now button;
  • current failure/backoff message.

A qBittorrent service detail/editor surface should also contain:

  • stale-data banner;
  • user-selectable chart windows (for example 5m, 30m, 1h, all retained);
  • speed chart sourced from read-only sample data;
  • paginated run-history table with trigger, status, duration, timestamp, and safe error detail;
  • loading, empty, disabled, and failed states.

Add typed API functions, React Query hooks, and types under the existing frontend/src/api, frontend/src/hooks, and frontend/src/types patterns. Poll status/history at a slower UI cadence than the sampler; the UI must not drive collection.

Observability

Add secret-safe metrics using bounded labels:

  • scheduled action runs total by action and status;
  • scheduled action duration by action;
  • last successful run timestamp by action/service;
  • current consecutive failures or stale state by action/service.

Avoid labels containing URLs, usernames, API keys, raw errors, or unbounded widget IDs. Structured logs should include service ID, action key, trigger, status, duration, and request ID where available.

Security and operational constraints

  • Only registered action keys can execute.
  • Service credentials are loaded through existing decryption helpers and are never returned or persisted in run records.
  • Manual-run endpoints use existing JWT/API authentication.
  • The scheduler must stop cleanly during lifespan shutdown and should not leave a new thread running after tests finish.
  • The deployment documentation must state the one-worker scheduler constraint.

Open implementation details

  • Choose final module names and whether scheduler run storage belongs in a new service-data concern or a dedicated settings-store table.
  • Define exact retry cap and jitter values.
  • Decide whether the scheduler status endpoint returns one action or a list of registered actions.
  • Finalize chart window/downsampling behavior for the 24-hour/1,200-row maximum.