8c69911252
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)
178 lines
6.8 KiB
Markdown
178 lines
6.8 KiB
Markdown
# 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`
|
||
|
||
```python
|
||
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`
|
||
|
||
```sql
|
||
-- 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
|
||
|
||
```sql
|
||
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_id` → `default_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.py` — `run_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.py` — `SshTaskWidgetSource.fetch` delegates to
|
||
`run_saved_task`.
|
||
|
||
### Modify (frontend)
|
||
|
||
- `types/index.ts` — field rename + `SavedTaskRun` alignment.
|
||
- `api/client.ts` — `runTask` sends `service_id`.
|
||
- `pages/Actions.tsx` — service selectors + history source.
|
||
|
||
## 8. Slice boundaries
|
||
|
||
1. **Backend** — `run_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 ~600–800 changed lines across two PRs.
|