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:
@@ -0,0 +1,68 @@
|
||||
"""Built-in, service-less widget kinds.
|
||||
|
||||
These widgets do not talk to an external service and therefore have no
|
||||
``service_id``. They are kept out of the service registry (which models
|
||||
configurable external services) and live here as a small closed set.
|
||||
|
||||
Currently: ``backups`` (reads the internal backup tables) and ``static``
|
||||
(plain text/markdown).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from media_library_viewer_api.integrations.base import WidgetKind
|
||||
|
||||
BUILTIN_WIDGET_KINDS: dict[str, WidgetKind] = {
|
||||
"backups": WidgetKind(
|
||||
kind="backups",
|
||||
name="Backups",
|
||||
description="Backup job summary and active alerts.",
|
||||
config_schema={"type": "object", "properties": {}, "required": []},
|
||||
default_config={},
|
||||
refresh_interval_ms=60_000,
|
||||
),
|
||||
"static": WidgetKind(
|
||||
kind="static",
|
||||
name="Static text",
|
||||
description="Plain text or markdown note.",
|
||||
config_schema={
|
||||
"type": "object",
|
||||
"properties": {"text": {"type": "string", "description": "Text or markdown content"}},
|
||||
"required": ["text"],
|
||||
},
|
||||
default_config={"text": ""},
|
||||
refresh_interval_ms=0,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_builtin_widget_kind(kind: str) -> WidgetKind | None:
|
||||
return BUILTIN_WIDGET_KINDS.get(kind)
|
||||
|
||||
|
||||
def is_builtin_kind(kind: str) -> bool:
|
||||
return kind in BUILTIN_WIDGET_KINDS
|
||||
|
||||
|
||||
def builtin_widget_kind_models() -> dict[str, type]:
|
||||
"""Pydantic widget-config models for built-in kinds (validated manually).
|
||||
|
||||
Backups has no user fields; static validates ``text``.
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class StaticConfig(BaseModel):
|
||||
text: str = Field(default="")
|
||||
|
||||
return {"static": StaticConfig}
|
||||
|
||||
|
||||
def validate_builtin_config(kind: str, config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate (lightly) a built-in widget config and return the cleaned dict."""
|
||||
models = builtin_widget_kind_models()
|
||||
model_cls = models.get(kind)
|
||||
if model_cls is None:
|
||||
return dict(config or {})
|
||||
return model_cls.model_validate(config or {}).model_dump(exclude_none=True)
|
||||
Reference in New Issue
Block a user