10fd4ead4a
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.
69 lines
2.1 KiB
Python
69 lines
2.1 KiB
Python
"""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)
|