feat(services): cleanup, services admin UI, docs
PR 4a of the runtime service registry change. - Remove addon pages (/addons/:addonId, AddonPage, addons/*) superseded by service pages. - Remove grafana_url/prometheus_url from backend config, compose, .env.example, and README (URLs now live on service records; VITE_ frontend deep-link vars retained). - Add Services page (/services) with create/list/delete + sidebar nav, so services are configurable in the tool itself and service pages are reachable. - Update docs/REQUIREMENTS.md service-registry section; add CHANGELOG.md with the breaking-upgrade note (MANAGE_ENCRYPTION_KEY required; grafana/prometheus env vars removed; default widget seeding removed). Verification: backend ruff clean, pytest 222 passed; frontend lint 0 errors, build success, 70 tests passed.
This commit is contained in:
+58
-33
@@ -256,54 +256,79 @@ fully removed (web-ui-rework; see decision log 2026-06-17).
|
||||
- Job templates should remain centralized in `jobs.py` for future extension.
|
||||
- Remote job template values must be shell-quoted before execution.
|
||||
|
||||
## Configurable Dashboard Widgets
|
||||
## Service Registry and Dashboard Widgets
|
||||
|
||||
### Overview
|
||||
|
||||
The dashboard is composed of persisted widget instances stored in the backend SQLite
|
||||
settings database. Each widget has a type, title, configuration, enabled flag, and
|
||||
sort order. The frontend renders enabled widgets in sort order and fetches data
|
||||
independently through the backend source adapters.
|
||||
External services (Grafana, Prometheus, Jellyfin, Nextcloud, SSH task runner) are
|
||||
configured **in the app** and persisted in the backend SQLite database. Each
|
||||
service instance holds non-secret config plus encrypted secret fields. Dashboard
|
||||
widgets are either **service-bound** (reference a service instance + a widget
|
||||
kind declared by that service) or **built-in / service-less** (backups, static
|
||||
text).
|
||||
|
||||
### Widget types
|
||||
Service definitions live as Pydantic modules in the backend
|
||||
(`integrations/`); they declare the service config schema, secret fields, and
|
||||
the widget kinds the service provides. There is no runtime plugin loading.
|
||||
|
||||
- **Jellyfin activity** — live sessions and idle users from a configured Jellyfin machine.
|
||||
- **Backups** — backup job summary and active alerts.
|
||||
- **Grafana link** — deep-link to a Grafana dashboard or panel (no iframe embedding).
|
||||
- **Prometheus metric** — result of a PromQL instant query.
|
||||
- **SSH task output** — output of a saved task run on a machine.
|
||||
### Services
|
||||
|
||||
- **Grafana** — base URL + optional API key; provides a dashboard-link widget.
|
||||
- **Prometheus** — base URL + optional bearer token; provides a PromQL metric widget.
|
||||
- **Jellyfin** — base URL + API key; provides a live-activity widget.
|
||||
- **Nextcloud** — base URL + app password (no widgets yet).
|
||||
- **SSH task runner** — host/port/username + saved SSH key reference + optional
|
||||
passphrase; provides a task-output widget. Tasks stay in the global saved-task
|
||||
registry; every run is recorded in `service_task_runs` as history.
|
||||
|
||||
Multiple instances per service type are supported. Services are managed from the
|
||||
**Services** page (`/services`) and each instance has a detail page at
|
||||
`/services/:serviceType/:serviceId`.
|
||||
|
||||
### Built-in widgets
|
||||
|
||||
- **Backups** — internal backup job summary and active alerts.
|
||||
- **Static text** — plain text or markdown note.
|
||||
|
||||
These do not reference a service.
|
||||
|
||||
### Security
|
||||
|
||||
- Widget `config` may not contain credential keys such as `password`, `token`,
|
||||
`secret`, `api_key`, `private_key`, or `passphrase`, or values that look like
|
||||
secrets (e.g., base64 blobs, `sk-` prefixes).
|
||||
- Widgets reuse machine-level Jellyfin/SSH credentials and environment settings for
|
||||
Grafana/Prometheus URLs; no secrets are stored in widget configuration.
|
||||
- SSH task widgets only run tasks from the saved-task registry; arbitrary commands
|
||||
are not accepted.
|
||||
|
||||
### Addon pages
|
||||
|
||||
Each non-core addon gets a dedicated page at `/addons/:addonId`:
|
||||
|
||||
- `/addons/grafana`
|
||||
- `/addons/prometheus`
|
||||
- `/addons/ssh-tasks`
|
||||
|
||||
Unknown addons render a "not installed" alert.
|
||||
- Service secrets (API keys, tokens, passphrases) are **encrypted at rest** with
|
||||
Fernet using a single env-provided `MANAGE_ENCRYPTION_KEY`, which is always
|
||||
required to start the backend.
|
||||
- Widget `config` and service `config` may not contain credential keys or
|
||||
secret-looking values; secrets go in the dedicated secret fields only.
|
||||
- Plaintext secrets are never returned by the API; only `secrets_set` flags are
|
||||
surfaced.
|
||||
- SSH task widgets only run tasks from the saved-task registry; arbitrary
|
||||
commands are not accepted.
|
||||
|
||||
### API
|
||||
|
||||
- `GET /api/widgets/sources` — list source types.
|
||||
- `GET /api/widgets/types` — list widget type metadata.
|
||||
- `GET /api/services/types` — service definition metadata (config schema,
|
||||
secret fields, widget kinds).
|
||||
- `GET /api/services/instances` — list service instances (no plaintext secrets).
|
||||
- `POST /api/services/instances` — create instance.
|
||||
- `PUT /api/services/instances/{id}` — update instance.
|
||||
- `DELETE /api/services/instances/{id}` — delete instance (cascade-deletes
|
||||
widgets referencing it).
|
||||
- `GET /api/widgets/builtin` — built-in (service-less) widget kinds.
|
||||
- `GET /api/widgets/instances` — list widget instances.
|
||||
- `POST /api/widgets/instances` — create instance.
|
||||
- `PUT /api/widgets/instances/{id}` — update instance.
|
||||
- `DELETE /api/widgets/instances/{id}` — delete instance.
|
||||
- `POST/PUT/DELETE /api/widgets/instances/{id}` — widget CRUD.
|
||||
- `GET /api/widgets/instances/{id}/data` — fetch widget data.
|
||||
|
||||
### Breaking change
|
||||
|
||||
Grafana/Prometheus URLs and credentials moved from environment variables into
|
||||
service records. The legacy `GRAFANA_URL` / `PROMETHEUS_URL` backend settings and
|
||||
the widget/addon-pages model were removed. `MANAGE_ENCRYPTION_KEY` is now required.
|
||||
|
||||
> **Follow-up (not in this change):** machine-level Jellyfin/Jellyseerr app
|
||||
> config still powers the Media/Users/Files pages. Migrating those onto the
|
||||
> service registry (and removing the machine app fields) is a separate change;
|
||||
> see `openspec/changes/service-registry/design.md` §12.5.
|
||||
|
||||
## Decision Log
|
||||
|
||||
- 2026-06-17: Decommissioned the legacy Manage-side system-metric scraping. Removed the backend `MonitoringPoller` (SSH-ran `df` on every machine every 5 min into a local SQLite `monitoring_machine_actions` table), the entire `services/monitoring_actions.py` module, the `/api/monitoring/poller`, `/api/monitoring/machines/{id}/actions`, and `/api/monitoring/disk` endpoints, the `monitoring_machine_actions` table (DROP on startup), the three `monitoring_poll_*` / `monitoring_action_retention_days` config knobs, and the orphaned frontend `DiskSpaceCard` + `DiskSpace` type. System metrics are now owned exclusively by Prometheus + node_exporter + Grafana. Kept the Alertmanager proxy (`/alerts`, `/alertmanager-status`, `/alertmanager-webhook`), `/prometheus-targets`, `/machines`, the `node_exporter_*` machine fields, and the on-demand `disk_usage` job template.
|
||||
|
||||
Reference in New Issue
Block a user