feat(widgets): rebind widgets to the service registry
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.
This commit is contained in:
@@ -180,6 +180,11 @@ class SettingsStore:
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_dashboard_widgets_sort ON dashboard_widgets(sort_order)")
|
||||
widget_cols = {row[1] for row in conn.execute("PRAGMA table_info(dashboard_widgets)").fetchall()}
|
||||
if "service_id" not in widget_cols:
|
||||
conn.execute("ALTER TABLE dashboard_widgets ADD COLUMN service_id TEXT")
|
||||
if "widget_kind" not in widget_cols:
|
||||
conn.execute("ALTER TABLE dashboard_widgets ADD COLUMN widget_kind TEXT")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS backup_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -453,39 +458,13 @@ class SettingsStore:
|
||||
)
|
||||
|
||||
def _seed_dashboard_widgets(self) -> None:
|
||||
"""Seed default dashboard widgets only when the table is empty."""
|
||||
from media_library_viewer_api.widgets.registry import WIDGET_REGISTRY
|
||||
"""Default widget seeding was removed.
|
||||
|
||||
self.init_schema()
|
||||
with self.connect() as conn:
|
||||
row = conn.execute("SELECT COUNT(*) FROM dashboard_widgets").fetchone()
|
||||
if row and int(row[0]) > 0:
|
||||
return
|
||||
defaults = [
|
||||
{
|
||||
"id": "jellyfin-activity-default",
|
||||
"addon_id": "core",
|
||||
"widget_type": "jellyfin",
|
||||
"title": "Jellyfin activity",
|
||||
"config": {"machine_id": ""},
|
||||
"enabled": True,
|
||||
"sort_order": 0,
|
||||
},
|
||||
{
|
||||
"id": "backups-summary-default",
|
||||
"addon_id": "backups",
|
||||
"widget_type": "backups",
|
||||
"title": "Backups",
|
||||
"config": {},
|
||||
"enabled": True,
|
||||
"sort_order": 1,
|
||||
},
|
||||
]
|
||||
for widget in defaults:
|
||||
info = WIDGET_REGISTRY.get(widget["widget_type"])
|
||||
if not info or info["addon_id"] != widget["addon_id"]:
|
||||
continue
|
||||
self.upsert_widget(widget)
|
||||
Widgets are now service-bound (or built-in). A fresh install starts with
|
||||
no widgets; the user configures services and adds widgets from the UI.
|
||||
Kept as a no-op so :meth:`ensure_defaults` callers are unchanged.
|
||||
"""
|
||||
return None
|
||||
|
||||
def ensure_defaults(self) -> None:
|
||||
self.init_schema()
|
||||
@@ -493,7 +472,6 @@ class SettingsStore:
|
||||
row = conn.execute("SELECT COUNT(*) FROM monitoring_machines").fetchone()
|
||||
if not row or int(row[0]) == 0:
|
||||
self._seed_local_machine()
|
||||
self._seed_dashboard_widgets()
|
||||
|
||||
def list_machines(self) -> list[dict[str, Any]]:
|
||||
self.init_schema()
|
||||
@@ -1404,10 +1382,13 @@ class SettingsStore:
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _row_to_widget(self, row: sqlite3.Row) -> dict[str, Any]:
|
||||
keys = row.keys()
|
||||
return {
|
||||
"id": row["id"],
|
||||
"addon_id": row["addon_id"],
|
||||
"widget_type": row["widget_type"],
|
||||
"service_id": row["service_id"] if "service_id" in keys else None,
|
||||
"widget_kind": row["widget_kind"] if "widget_kind" in keys else None,
|
||||
"title": row["title"],
|
||||
"config": json.loads(row["config_json"] or "{}"),
|
||||
"enabled": bool(row["enabled"]),
|
||||
@@ -1423,8 +1404,11 @@ class SettingsStore:
|
||||
) -> dict[str, Any]:
|
||||
current = self.get_widget(widget_id) if widget_id else None
|
||||
widget_id = str(payload.get("id") or widget_id or uuid.uuid4().hex[:12]).strip() or uuid.uuid4().hex[:12]
|
||||
addon_id = str(payload.get("addon_id") or (current or {}).get("addon_id", "")).strip()
|
||||
widget_type = str(payload.get("widget_type") or (current or {}).get("widget_type", "")).strip()
|
||||
service_id = (
|
||||
str(payload.get("service_id") or (current or {}).get("service_id") or "").strip()
|
||||
or None
|
||||
)
|
||||
widget_kind = str(payload.get("widget_kind") or (current or {}).get("widget_kind", "")).strip()
|
||||
title = str(payload.get("title") or (current or {}).get("title", "") or "").strip()
|
||||
config = payload.get("config", (current or {}).get("config", {}))
|
||||
if not isinstance(config, dict):
|
||||
@@ -1433,10 +1417,14 @@ class SettingsStore:
|
||||
_validate_config_keys(config)
|
||||
enabled = bool(payload.get("enabled", (current or {}).get("enabled", True)))
|
||||
sort_order = int(payload.get("sort_order", (current or {}).get("sort_order", 0)) or 0)
|
||||
# Legacy label kept for diagnostics; new code uses service_id + widget_kind.
|
||||
widget_type = f"{service_id}:{widget_kind}" if widget_kind else ""
|
||||
return {
|
||||
"id": widget_id,
|
||||
"addon_id": addon_id,
|
||||
"addon_id": "",
|
||||
"widget_type": widget_type,
|
||||
"service_id": service_id,
|
||||
"widget_kind": widget_kind,
|
||||
"title": title,
|
||||
"config": config,
|
||||
"enabled": enabled,
|
||||
@@ -1470,13 +1458,15 @@ class SettingsStore:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO dashboard_widgets (
|
||||
id, addon_id, widget_type, title, config_json, enabled,
|
||||
sort_order, created_at, updated_at
|
||||
id, addon_id, widget_type, service_id, widget_kind, title,
|
||||
config_json, enabled, sort_order, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
addon_id = excluded.addon_id,
|
||||
widget_type = excluded.widget_type,
|
||||
service_id = excluded.service_id,
|
||||
widget_kind = excluded.widget_kind,
|
||||
title = excluded.title,
|
||||
config_json = excluded.config_json,
|
||||
enabled = excluded.enabled,
|
||||
@@ -1487,6 +1477,8 @@ class SettingsStore:
|
||||
widget["id"],
|
||||
widget["addon_id"],
|
||||
widget["widget_type"],
|
||||
widget["service_id"],
|
||||
widget["widget_kind"],
|
||||
widget["title"],
|
||||
json.dumps(widget["config"]),
|
||||
1 if widget["enabled"] else 0,
|
||||
|
||||
Reference in New Issue
Block a user