Four coupled changes to the services-as-hub IA:
1. Nav entries use service TYPE names (Jellyfin, SSH Tasks, Alertmanager,
Grafana, Prometheus, Backups, Authentik) instead of conceptual names
(Media, Files, Actions, Alerts, Users). ssh_tasks collapses to one
entry ('SSH Tasks') instead of two. The content tabs inside each
service page surface the concepts (Files, Actions).
2. Service page gains a two-level tab structure when multiple enabled
instances of the same type exist: instance tabs on top ([Main Jellyfin]
[Backup Jellyfin]), content tabs below ([Overview] [Media] [Requests]
[Widgets]). Clicking an instance tab navigates to the sibling's route.
Single instance: no instance tabs. Replaces the dropdown switcher.
3. Config tab (connection fields, secrets, enable/disable, delete) moves
from the service page to Settings > Services tab. The service page
becomes a PURE operational view (Overview + content tabs + Widgets) --
no save/delete/config state. Settings gains a 4th tab 'Services' with
ServiceConfigEditor per instance (schema-driven config fields, secrets
with leave-blank-to-keep semantics, ConfirmDialog on delete).
4. Overview tab is now a configurable widget grid per service instance.
Each instance manages its own set of widgets on its Overview. Backend
widget list endpoints gain ?service_id= and ?scope= (dashboard|service)
filter params; the main Dashboard uses scope=dashboard to exclude
service-scoped widgets. The OverviewTab reuses WidgetInstanceCard +
WidgetConfigDialog. Empty state CTA for instances with no widgets.
All service-tab stubs are replaced; stubs.tsx deleted.
272 backend tests pass (+1 widget filter); 121 frontend tests pass (+3
instance-tabs + OverviewTab); lint/build green both sides.
Slice 2 of observability-service-registry. The monitoring router resolves
observability components from the service registry instead of env vars.
- routers/monitoring.py: removed _alertmanager_client/_webhook_client env
readers + the get_settings import. Added _resolve_service_record(store,
service_type, service_id?) -> ServiceRecord|None (requested instance with
type+enabled checks, else first enabled instance), plus _base_url/_timeout/
_auth_headers (Bearer from api_key)/_status_response helpers.
- /alerts + /alertmanager-status now take service_id? + Depends(store),
resolve an alertmanager service, return graceful not-configured/
unreachable payloads including service_id/name; status down-branches now
include peers:[] + error (fixes prior type drift).
- NEW /grafana-status (probes /api/health) and /prometheus-status (probes
/-/healthy then /api/v1/status/buildinfo) returning
{up,version,service_id,name,error}.
- Webhook receiver is now log-only (dropped the outbound
ALERTMANAGER_WEBHOOK_URL forward).
- tests: rewrote TestAlertmanager + TestAlertmanagerWebhook to mock
_resolve_service_record/requests.get (not-configured via empty registry);
added TestGrafanaStatus/TestPrometheusStatus and a TestResolveServiceRecord
unit class covering service_id match/type-mismatch/disabled and first-
enabled/none-enabled paths.
Orphaned config fields alertmanager_url/alertmanager_webhook_url and the
env-var removal land in Slice 5. ruff clean; 240 backend tests pass.
Reviewed fresh-context (read-only): no blockers.
Manage now connects to existing Grafana/Prometheus/Alertmanager instances
and never deploys its own stack.
- docker-compose.yml / docker-compose.dev.yml: removed prometheus, loki,
alloy, grafana, alertmanager, node-exporter services, the monitoring
network, and observability named volumes; they now ship only backend +
frontend. Dev frontend now joins the web network so the Vite dev proxy
can reach the backend.
- backend: alertmanager_url default is now empty; /api/monitoring/alerts
and /alertmanager-status return graceful "not configured" responses
when ALERTMANAGER_URL is unset. Added not-configured tests.
- docker-compose.observability.yml: kept as the optional standalone
example; header clarifies Manage does not deploy it.
- Removed orphaned combined monitoring/prometheus/prometheus.yml
(standalone stack uses prometheus.standalone.yml).
- Docs (README, REQUIREMENTS decision log, monitoring-logging-design,
observability-runbooks, context.md, MIGRATION_PLAN, frontend/README,
CHANGELOG) updated to the connect-to-existing model.
VITE_GRAFANA_URL / VITE_PROMETHEUS_URL remain as optional frontend
deep-link overrides. .env.example still needs a manual update (safety
policy blocks assistant edits): set ALERTMANAGER_URL empty/optional and
move standalone-only vars out of the root file.
- Add shared task_runner.run_saved_task helper used by routers/tasks.py and
widgets/sources.py SshTaskWidgetSource.
- Saved tasks now target ssh_tasks service instances via default_service_id;
the legacy default_machine_id and saved_task_runs are removed.
- Actions page lists ssh_tasks services for default and run-time selection.
- Update types, API client, hooks, tests, docs, and changelog.
Backend tests: 222 passed. Frontend lint/build/test: clean (71 passed).
PR 2 of 4 for the runtime service registry change.
- dashboard_widgets gains service_id + widget_kind columns (legacy
addon_id/widget_type kept but unused).
- Source adapters take (service: ServiceRecord | None, widget_kind, config).
SERVICE_ADAPTERS keyed by service_type; BUILTIN_ADAPTERS for backups/static.
- Backups and static stay as service-less built-ins (service_id nullable),
exposed via GET /api/widgets/builtin.
- SSH task adapter resolves the task + instance, runs over SSH, and appends a
service_task_runs history row on success/failure/timeout/error.
- Retire widgets/registry.py; widget metadata now comes from the integrations
registry + widgets/builtin. Remove /api/widgets/types and /api/widgets/sources.
- Stop default widget seeding (fresh install = empty dashboard).
- Rewrite widget tests around the service-bound + built-in model (26 tests).
Backend-only breaking change; frontend is reconciled in Slice 3. Build/lint
stay green; pytest 222 passed.