import type { ComponentType } from "react"; import { AlertmanagerAlertsWidget } from "../widgets/AlertmanagerAlertsWidget"; import { BackupsWidget } from "../widgets/BackupsWidget"; import { GrafanaLinkWidget } from "../widgets/GrafanaLinkWidget"; import { PrometheusChartWidget } from "../widgets/PrometheusChartWidget"; import { JellyfinWidget } from "../widgets/JellyfinWidget"; import { JellyfinNowPlayingWidget } from "../widgets/JellyfinNowPlayingWidget"; import { PrometheusMetricWidget } from "../widgets/PrometheusMetricWidget"; import { SshTaskWidget } from "../widgets/SshTaskWidget"; import { StaticWidget } from "../widgets/StaticWidget"; import type { ServiceInstance, ServiceTypeInfo, WidgetInstance, } from "../types"; /** * Closed frontend registry mirroring the backend service definitions. * * Each service type maps its widget kinds to a presentational component and a * refresh interval. Built-in (service-less) kinds are listed separately. */ export interface WidgetComponentProps { widget: WidgetInstance; refreshIntervalMs: number; description?: string; } export interface ServiceWidgetBinding { kind: string; name: string; description: string; refreshIntervalMs: number; defaultConfig: Record; configSchema: Record; component: ComponentType; } export interface ServiceBinding { serviceType: string; name: string; description: string; widgets: ServiceWidgetBinding[]; } export const SERVICE_REGISTRY: Record = { alertmanager: { serviceType: "alertmanager", name: "Alertmanager", description: "Alertmanager alerts and status.", widgets: [ { kind: "active_alerts", name: "Active alerts", description: "Firing alerts summary from Alertmanager.", refreshIntervalMs: 30_000, defaultConfig: {}, configSchema: { type: "object", properties: { severity_filter: { type: "string" }, }, required: [], }, component: AlertmanagerAlertsWidget, }, ], }, grafana: { serviceType: "grafana", name: "Grafana", description: "Dashboards, metrics, and logs.", widgets: [ { kind: "link", name: "Dashboard link", description: "Deep-link to a Grafana dashboard or panel.", refreshIntervalMs: 0, defaultConfig: { dashboard_uid: "" }, configSchema: { type: "object", properties: { dashboard_uid: { type: "string" }, panel_id: { type: "integer" }, }, required: ["dashboard_uid"], }, component: GrafanaLinkWidget, }, ], }, prometheus: { serviceType: "prometheus", name: "Prometheus", description: "Metrics storage and PromQL queries.", widgets: [ { kind: "metric", name: "Metric", description: "Instant query result rendered as a metric.", refreshIntervalMs: 30_000, defaultConfig: { promql: "" }, configSchema: { type: "object", properties: { promql: { type: "string" } }, required: ["promql"], }, component: PrometheusMetricWidget, }, { kind: "chart", name: "Chart", description: "Multi-series line chart from a PromQL range query.", refreshIntervalMs: 60_000, defaultConfig: { promql: "", window: "1h" }, configSchema: { type: "object", properties: { promql: { type: "string", description: "PromQL range query expression", }, window: { type: "string", description: "Time window preset (1h, 6h, 24h, 7d)", }, }, required: ["promql"], }, component: PrometheusChartWidget, }, ], }, jellyfin: { serviceType: "jellyfin", name: "Jellyfin", description: "Media server with live session activity.", widgets: [ { kind: "activity", name: "Activity", description: "Live sessions and idle users.", refreshIntervalMs: 30_000, defaultConfig: {}, configSchema: { type: "object", properties: {}, required: [] }, component: JellyfinWidget, }, { kind: "now_playing", name: "Now Playing", description: "Only sessions actively playing media.", refreshIntervalMs: 30_000, defaultConfig: {}, configSchema: { type: "object", properties: {}, required: [] }, component: JellyfinNowPlayingWidget, }, ], }, nextcloud: { serviceType: "nextcloud", name: "Nextcloud", description: "Self-hosted files and collaboration.", widgets: [], }, ssh_tasks: { serviceType: "ssh_tasks", name: "SSH task runner", description: "Run reusable saved tasks over SSH and keep run history.", widgets: [ { kind: "task_output", name: "Task output", description: "Output of a saved task run.", refreshIntervalMs: 0, defaultConfig: { task_id: "" }, configSchema: { type: "object", properties: { task_id: { type: "string" } }, required: ["task_id"], }, component: SshTaskWidget, }, ], }, }; export const BUILTIN_WIDGETS: Record = { backups: { kind: "backups", name: "Backups", description: "Backup job summary and active alerts.", refreshIntervalMs: 60_000, defaultConfig: {}, configSchema: { type: "object", properties: {}, required: [] }, component: BackupsWidget, }, static: { kind: "static", name: "Static text", description: "Plain text or markdown note.", refreshIntervalMs: 0, defaultConfig: { text: "" }, configSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"], }, component: StaticWidget, }, }; export function getServiceBinding( serviceType: string, ): ServiceBinding | undefined { return SERVICE_REGISTRY[serviceType]; } export function getBuiltinBinding( kind: string, ): ServiceWidgetBinding | undefined { return BUILTIN_WIDGETS[kind]; } export interface ResolvedWidget { component: ComponentType; description: string; refreshIntervalMs: number; } /** * Resolve a widget instance to its component + metadata. * * Service-bound widgets are resolved via the parent service's type (looked up * from the services list); built-in widgets are resolved directly. */ export function resolveWidget( widget: WidgetInstance, services: ServiceInstance[], ): ResolvedWidget | undefined { if (widget.service_id) { const service = services.find((s) => s.id === widget.service_id); if (!service) return undefined; const binding = getServiceBinding(service.service_type); const widgetBinding = binding?.widgets.find( (w) => w.kind === widget.widget_kind, ); if (!widgetBinding) return undefined; return { component: widgetBinding.component, description: widgetBinding.description, refreshIntervalMs: widgetBinding.refreshIntervalMs, }; } const builtin = getBuiltinBinding(widget.widget_kind); if (!builtin) return undefined; return { component: builtin.component, description: builtin.description, refreshIntervalMs: builtin.refreshIntervalMs, }; } /** Merge backend type metadata (config_schema, secret_fields) onto bindings. */ export function enrichServiceTypes( types: ServiceTypeInfo[], ): ServiceTypeInfo[] { return types; }