Design-only artifacts for unifying saved tasks on ssh_tasks services. No implementation yet. - proposal: two-path problem (Actions→machine vs widget→service), goals, non-goals, grilling decisions (SSH-only, service_task_runs only, keep override) - design: shared run_saved_task helper, column rename, saved_task_runs dropped, API + frontend changes, 2-slice plan - tasks: backend (shared runner + router) + frontend (Actions page)
6.8 KiB
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 callrun_saved_task, removing its inline copy. routers/tasks.pyrun_taskcallsrun_saved_taskinstead of_client_for_machine+record_task_run._build_ssh_client(currently private inwidgets/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; ifservice_idquery param is given, use it (override), else usetask.default_service_id; load thessh_tasksservice record; build aServiceRecord(decrypt secrets); callrun_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_id→default_service_id.SavedTaskRunfields align withservice_task_runs(service_id,exit_status,stdout_tail, …).
5.2 API client + hooks
runTask(taskId, serviceId?)sendsservice_id.fetchSavedTaskRuns(taskId)reads/api/tasks/{id}/runs(nowservice_task_runs-backed).
5.3 Actions page
- Task editor: "Default service"
<Select>listsssh_tasksservice instances (viauseServiceInstances("ssh_tasks")), not machines. - Run dialog: "Run on"
<Select>listsssh_tasksinstances (override). - Run history: reads the task's
service_task_runs. useMonitoringSettingsremoved from the Actions page (no longer needed).
6. Migration and breaking changes
- DB:
saved_tasks.default_machine_idrenamed todefault_service_id(existing values become stale references to machine ids; inert — the user re-points).saved_task_runsdropped. - Local execution removed. Deployments relying on local tasks must use an
ssh_tasksservice (e.g. pointing at localhost with a key). - Changelog + README note the breaking change.
7. File-level plan
Create (backend)
services/task_runner.py—run_saved_taskshared helper.
Modify (backend)
services/settings_store.py— rename column; dropsaved_task_runs+record_task_run/list_task_runs(task-run flavor).routers/tasks.py— service resolution; callrun_saved_task;service_idparam; readservice_task_runs.widgets/sources.py—SshTaskWidgetSource.fetchdelegates torun_saved_task.
Modify (frontend)
types/index.ts— field rename +SavedTaskRunalignment.api/client.ts—runTasksendsservice_id.pages/Actions.tsx— service selectors + history source.
8. Slice boundaries
- Backend —
run_saved_taskhelper; saved_tasks column rename; tasks router rewired; widget delegates;saved_task_runsdropped; tests. - Frontend — types + API + Actions page rewire; tests.
Estimated ~600–800 changed lines across two PRs.