docs(service-registry): lock decisions (cascade delete, required key, SSH runner model)

- §11 decisions: cascade-delete services with widgets; MANAGE_ENCRYPTION_KEY
  always required; SSH task runner is multi-instance with reusable tasks.
- §12 SSH task runner model: instances absorb SSH task transport, tasks stay
  global/reusable with default_service_id, service_task_runs logs history,
  widget config { task_id, service_id? }.
- tasks.md: add service_task_runs table + cascade-delete tests to Slice 1,
  SSH run-logging to Slice 2, follow-ups (Actions rebuild, machine unification).
This commit is contained in:
Developer
2026-06-22 11:14:01 +00:00
parent 9782280a03
commit 9459de5c07
2 changed files with 99 additions and 18 deletions
+77 -10
View File
@@ -222,7 +222,7 @@ def decrypt_secrets(blob: dict[str, str]) -> dict[str, str]: ...
| GET | `/api/services` | List service instances (no plaintext secrets; only "set" flags). |
| POST | `/api/services` | Create instance (validates type, config, secret schema). |
| PUT | `/api/services/{id}` | Update instance. |
| DELETE | `/api/services/{id}` | Delete instance (and refuse while widgets reference it, or cascade). |
| DELETE | `/api/services/{id}` | Delete instance; **cascade-deletes** widgets referencing it in the same transaction. |
### Widgets (unchanged paths, new semantics)
@@ -341,13 +341,80 @@ Each slice keeps `pytest`, `ruff`, `npm run lint`, and `npm run build` green.
Estimated total: ~2,0002,400 changed lines across four PRs.
## 11. Open questions to resolve before apply
## 11. Decisions resolved
1. Should deleting a service that still has widgets block (return 409) or cascade-delete
the widgets? Recommend **block with 409** and require the user to remove widgets first.
2. Should `MANAGE_ENCRYPTION_KEY` have a development default (e.g. derived from a fixed
dev key when `AUTH_ENABLED=false`)? Recommend **no** — require it always to avoid
accidental plaintext in dev.
3. Does the SSH task runner service hold the SSH key reference, or does it reference a
machine? Recommend the service record holds `machine_id` (transport) + optional
task-scoped overrides; the saved-task registry stays unchanged.
1. **Deleting a service that still has widgets****cascade delete.** The store deletes
every `dashboard_widgets` row referencing the service inside the same transaction as
the service delete. Simple and safe in SQLite; no 409 pre-check.
2. **`MANAGE_ENCRYPTION_KEY` dev default** → **always required.** No fallback, even when
`AUTH_ENABLED=false`. Startup fails fast if it is missing or not a valid Fernet key.
3. **SSH task runner shape****multi-instance, reusable tasks, persisted run history.**
See §12 for the full model.
## 12. SSH task runner model
The SSH task runner is the most involved service type. Instances absorb the SSH task
execution role currently held by machines; tasks stay global and reusable; every
invocation is logged.
### 12.1 Instances
- `service_type = "ssh_tasks"`.
- Each instance is an SSH endpoint: `host`, `port`, `username`, `ssh_key_id`, optional
`passphrase`. Connection config lives on the service record; the SSH key itself stays
in the existing saved-key registry (referenced by `ssh_key_id`).
- Multi-instance by design ("home server", "media box", …).
### 12.2 Tasks (global, reusable)
- Saved tasks remain a **global** registry (`name`, `task_type` shell/python, `content`,
`enabled`). A task is **not** owned by an instance.
- Each task gains `default_service_id` (replaces the old `default_machine_id`) — the
instance it targets by default. At run time the caller may override the target
instance.
- A task can therefore run against any instance; the link is captured per-run.
### 12.3 Run history (logs)
A new `service_task_runs` table records every invocation:
```sql
CREATE TABLE IF NOT EXISTS service_task_runs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
service_id TEXT NOT NULL,
status TEXT NOT NULL, -- success | failure | timeout | error
exit_status INTEGER,
duration_ms INTEGER,
stdout_tail TEXT,
stderr_tail TEXT,
error TEXT,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_service_task_runs_service ON service_task_runs(service_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_service_task_runs_task ON service_task_runs(task_id, created_at DESC);
```
- Populated by the SSH task adapter on every widget data fetch and by the Actions
runner on manual runs.
- Surfaced on the instance's service page as a log/history list, and on the task detail
as recent runs.
- Replaces the legacy `saved_task_runs` concept once the Actions page is rebuilt on
services (Slice 4 / a follow-up).
### 12.4 SSH task widget
Widget config for `ssh_tasks` becomes `{ task_id, service_id? }`:
- If `service_id` is omitted, the task's `default_service_id` is used.
- The adapter loads the task, resolves the instance, runs it, appends a
`service_task_runs` row, and returns the trimmed stdout/stderr/exit status.
### 12.5 Relationship to machines
- The SSH task execution role moves **out of machines** into `ssh_tasks` instances.
- Machines **keep** their role for the File Browser and node_exporter monitoring
transport in this change, to avoid also reworking Files/Monitoring here.
- Practical consequence: an SSH host used for both files and tasks may be defined twice
(once as a machine, once as an ssh_tasks instance) during the transition. Unifying
machines under services is an explicit **follow-up change**, not part of this one.