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.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user