Files
manage/openspec/changes/archive/unify-tasks-on-services/design.md
T
Developer ca8927834e chore(openspec): archive completed changes
Move finished change directories to openspec/changes/archive/:
- configurable-dashboard-widgets
- decommission-monitoring-poller
- service-registry
- unify-tasks-on-services

All associated implementation has been merged to main.
2026-06-23 19:38:34 +00:00

6.8 KiB
Raw Blame History

Design: Unify Saved Tasks on SSH Services

Change: unify-tasks-on-services Phase: design Date: 2026-06-19

1. Architecture overview

                        ┌─────────────────────────────────────┐
                        │   saved_tasks (global, reusable)     │
                        │   default_service_id → ssh_tasks     │
                        └─────────────────────────────────────┘
                              │                    │
                  Actions page │                    │ SSH task widget
                              ▼                    ▼
                        ┌─────────────────────────────────────┐
                        │   run_saved_task(store, task, svc)   │  ← shared helper
                        │   build client → run → log           │
                        └─────────────────────────────────────┘
                                      │
                                      ▼
                        ┌─────────────────────────────────────┐
                        │   service_task_runs (one history)    │
                        └─────────────────────────────────────┘

Both the Actions runner and the SSH task widget call one shared helper, so there is a single execution path and a single history table.

2. Shared execution helper

New: backend/src/media_library_viewer_api/services/task_runner.py

from dataclasses import dataclass
from media_library_viewer_api.services.settings_store import SettingsStore
from media_library_viewer_api.widgets.sources import ServiceRecord, _build_ssh_client

@dataclass
class TaskRunResult:
    exit_status: int
    stdout: str
    stderr: str
    duration_ms: int
    status: str   # "success" | "failure" | "timeout" | "error"
    error: str

def run_saved_task(
    store: SettingsStore,
    task: dict,
    service: ServiceRecord,
    *,
    request_id: str = "",
) -> TaskRunResult:
    """Run a saved task on an ssh_tasks service instance and log it.

    Builds the SSH client from the service record, renders the command (shell or
    python3 -c), runs it with the service's timeout, appends a service_task_runs
    row, and returns the result.
    """
    ...
  • The widget adapter (SshTaskWidgetSource.fetch) is refactored to call run_saved_task, removing its inline copy.
  • routers/tasks.py run_task calls run_saved_task instead of _client_for_machine + record_task_run.
  • _build_ssh_client (currently private in widgets/sources.py) is promoted to the helper module or a shared location so both callers use it.

3. Data model changes

3.1 saved_tasks

-- default_machine_id replaced by default_service_id
ALTER TABLE saved_tasks RENAME COLUMN default_machine_id TO default_service_id;

In SQLite (3.25+) RENAME COLUMN is supported. The column still stores an id, now pointing at services.id (an ssh_tasks instance) instead of a machine.

3.2 saved_task_runs dropped

DROP TABLE IF EXISTS saved_task_runs;

All history lives in service_task_runs (added in the service-registry change). The record_task_run / list_task_runs methods on SettingsStore are removed.

4. Backend API

routers/tasks.py

Method Path Change
GET /api/tasks Unchanged (task now carries default_service_id).
POST /api/tasks TaskInput.default_service_id replaces default_machine_id.
PUT /api/tasks/{id} Same field rename.
DELETE /api/tasks/{id} Unchanged.
GET /api/tasks/{id}/runs Reads service_task_runs (filtered by task_id).
POST /api/tasks/run?service_id=... service_id replaces machine_id; resolves an ssh_tasks service (override) or the task's default_service_id; calls run_saved_task.

_resolve_machine_for_task and _client_for_machine are removed (replaced by service resolution + the shared helper).

Resolution + validation

  • run_task: load the task; if service_id query param is given, use it (override), else use task.default_service_id; load the ssh_tasks service record; build a ServiceRecord (decrypt secrets); call run_saved_task.
  • 400 if the task is disabled; 400 if no service resolves; 404 if the task or service is missing.

5. Frontend

5.1 Types

SavedTask / SavedTaskInput / SavedTaskRun (frontend/src/types/index.ts):

  • default_machine_iddefault_service_id.
  • SavedTaskRun fields align with service_task_runs (service_id, exit_status, stdout_tail, …).

5.2 API client + hooks

  • runTask(taskId, serviceId?) sends service_id.
  • fetchSavedTaskRuns(taskId) reads /api/tasks/{id}/runs (now service_task_runs-backed).

5.3 Actions page

  • Task editor: "Default service" <Select> lists ssh_tasks service instances (via useServiceInstances("ssh_tasks")), not machines.
  • Run dialog: "Run on" <Select> lists ssh_tasks instances (override).
  • Run history: reads the task's service_task_runs.
  • useMonitoringSettings removed from the Actions page (no longer needed).

6. Migration and breaking changes

  • DB: saved_tasks.default_machine_id renamed to default_service_id (existing values become stale references to machine ids; inert — the user re-points). saved_task_runs dropped.
  • Local execution removed. Deployments relying on local tasks must use an ssh_tasks service (e.g. pointing at localhost with a key).
  • Changelog + README note the breaking change.

7. File-level plan

Create (backend)

  • services/task_runner.pyrun_saved_task shared helper.

Modify (backend)

  • services/settings_store.py — rename column; drop saved_task_runs + record_task_run / list_task_runs (task-run flavor).
  • routers/tasks.py — service resolution; call run_saved_task; service_id param; read service_task_runs.
  • widgets/sources.pySshTaskWidgetSource.fetch delegates to run_saved_task.

Modify (frontend)

  • types/index.ts — field rename + SavedTaskRun alignment.
  • api/client.tsrunTask sends service_id.
  • pages/Actions.tsx — service selectors + history source.

8. Slice boundaries

  1. Backendrun_saved_task helper; saved_tasks column rename; tasks router rewired; widget delegates; saved_task_runs dropped; tests.
  2. Frontend — types + API + Actions page rewire; tests.

Estimated ~600800 changed lines across two PRs.