Add Jellyfin Now Playing + Grafana Panel embed widgets

Two new additive widget kinds:

Jellyfin 'now_playing': like the existing 'activity' widget but filters
to only sessions with active playback (NowPlayingItem present + not
paused). Shows who's actually watching right now. The 'activity' kind
is unchanged (shows all sessions including idle).

Grafana 'panel': embeds a single Grafana panel directly in the app via
an iframe, using Grafana's /d-solo/ endpoint (renders one panel without
dashboard chrome, kiosk=tv). Configurable dashboard_uid, panel_id, and
time range (from/to, defaults now-1h/now). Includes a fallback 'Open in
Grafana' link for when embedding is blocked by X-Frame-Options/CSP. The
'link' kind is unchanged (still builds a deep-link URL).

Backend: new widget configs + definitions on jellyfin/grafana; source
adapter logic (session filter for now_playing; d-solo embed URL for
panel); 6 new tests.

Frontend: JellyfinNowPlayingWidget + GrafanaPanelWidget components;
registry bindings; 6 new tests.

278 backend tests pass (+6); 127 frontend tests pass (+6); lint/build
green both sides.
This commit is contained in:
Developer
2026-06-28 15:26:43 +00:00
parent 8d2e4c9bfd
commit b877a32ad8
11 changed files with 453 additions and 4 deletions
@@ -26,6 +26,15 @@ class GrafanaLinkWidgetConfig(WidgetConfigBase):
panel_id: int | None = None
class GrafanaPanelWidgetConfig(WidgetConfigBase):
"""Embed a single Grafana panel via iframe."""
dashboard_uid: str
panel_id: int
from_ts: str = "now-1h"
to_ts: str = "now"
DEFINITION = ServiceDefinition(
service_type="grafana",
name="Grafana",
@@ -43,5 +52,13 @@ DEFINITION = ServiceDefinition(
default_config={"dashboard_uid": ""},
refresh_interval_ms=0,
),
widget_kind(
kind="panel",
name="Panel embed",
description="Embed a Grafana panel directly.",
model_cls=GrafanaPanelWidgetConfig,
default_config={"dashboard_uid": "", "panel_id": 1, "from_ts": "now-1h", "to_ts": "now"},
refresh_interval_ms=0,
),
],
)
@@ -36,6 +36,12 @@ class JellyfinActivityWidgetConfig(WidgetConfigBase):
pass
class JellyfinNowPlayingWidgetConfig(WidgetConfigBase):
"""Only show sessions with active playback (not idle/paused)."""
pass
DEFINITION = ServiceDefinition(
service_type="jellyfin",
name="Jellyfin",
@@ -53,5 +59,13 @@ DEFINITION = ServiceDefinition(
default_config={},
refresh_interval_ms=30_000,
),
widget_kind(
kind="now_playing",
name="Now Playing",
description="Only sessions actively playing media.",
model_cls=JellyfinNowPlayingWidgetConfig,
default_config={},
refresh_interval_ms=30_000,
),
],
)