Files
manage/openspec/changes/unify-tasks-on-services/design.md
T
Developer 8c69911252 docs(unify-tasks): SDD proposal, design, and tasks
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)
2026-06-23 13:05:52 +00:00

178 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ~600800 changed lines across two PRs.