Files
manage/.pi-tmp/grafana-chart-out.md
Developer 447775048c Replace Grafana iframe panel with server-side chart widget
The iframe-based 'panel' widget didn't work: the browser couldn't
authenticate against the OIDC-protected Grafana (Authentik), and
iframes can't carry Bearer tokens or share cross-origin session
cookies. Result: blank iframe or login redirect.

Replace it with a 'chart' widget that queries Grafana's datasource
API server-side:

Backend (GrafanaWidgetSource): POSTs to /api/ds/query with the stored
api_key (which bypasses OIDC), using the widget's configured PromQL
query, datasource_uid, time range, and resolution. Normalizes Grafana's
frame-based response into a simple {series: [{label, points: [{t, v}]}]}
shape. The api_key is never exposed to the browser.

Frontend (GrafanaChartWidget): renders the series data as a recharts
LineChart with dark-mode-aware colors (Tailwind --chart-* tokens),
responsive container, custom tooltip, and per-series lines. Loading
skeleton, error Alert, and empty state. recharts ^3.9.2 added.

The 'link' widget kind (deep-link URL) is unchanged. The 'panel' kind
and GrafanaPanelWidget are fully removed.

Backend: 279 tests pass (+1 net: -2 panel + 3 chart). Frontend: 127
tests pass (net 0: -3 panel + 3 chart). Lint/build green both sides.
2026-07-06 10:19:57 +00:00

6.9 KiB

Grafana Chart Widget — worker output

Files changed (10 files, ~400 lines)

File Status Lines
backend/src/media_library_viewer_api/integrations/grafana.py modified +12/-12 (panel→chart config + kind)
backend/src/media_library_viewer_api/widgets/sources.py modified +70/-12 (chart query adapter replaces panel URL logic)
backend/tests/test_widgets.py modified +55/-20 (3 new chart tests replace 2 panel tests)
backend/tests/test_services.py modified +2/-2 (grafana widget-kind + API-metadata assertions)
frontend/src/widgets/GrafanaChartWidget.tsx new 100
frontend/src/widgets/__tests__/GrafanaChartWidget.test.tsx new 57
frontend/src/widgets/GrafanaPanelWidget.tsx deleted -50
frontend/src/widgets/__tests__/GrafanaPanelWidget.test.tsx deleted -72
frontend/src/integrations/registry.ts modified +24/-14 (chart binding replaces panel)
frontend/src/integrations/registry.test.ts modified +1/-1 (panel→chart)
frontend/src/widgets/index.ts modified +1/-0 (export GrafanaChartWidget)
frontend/package.json + package-lock.json modified +1 dep (recharts ^3.9.2)

recharts version installed: ^3.9.2

Grafana /api/ds/query request/response shape

Request (POST):

{
  "queries": [{
    "datasource": {"uid": "prometheus", "type": "prometheus"},
    "expr": "rate(cpu[5m])",
    "format": "time_series",
    "intervalMs": 30000,
    "maxDataPoints": 100,
    "refId": "A"
  }],
  "from": "now-1h",
  "to": "now"
}

Headers: Authorization: Bearer {api_key}, Content-Type: application/json

Response (abbreviated):

{
  "results": {
    "A": {
      "frames": [{
        "data": { "values": [[1000, 2000], [0.5, 0.8]] },
        "schema": { "fields": [{"name":"Time"}, {"name":"cpu_usage"}] }
      }]
    }
  }
}

Series normalization logic

Iterates results[*].frames[]. For each frame with values having >=2 arrays (timestamps + values), extracts the series label from schema.fields[-1].name and zips timestamps+values into [{t: int, v: float|null}]. Returns {"series": [{"label": "...", "points": [...]}]}.

Frontend chart rendering

GrafanaChartWidget fetches widget data, extracts data.series, merges all series by timestamp into a single recharts data array ([{time, cpu_usage: 0.5, mem: 0.3}, ...]), and renders a <LineChart> with one <Line> per series. Uses Tailwind CSS variables (--chart-1 through --chart-5) for colors so it respects dark mode. Includes loading skeleton, error Alert, and empty-state Alert.

Validation

cd backend && .venv/bin/ruff check . && .venv/bin/python -m pytest  → 279 passed, ruff clean
cd frontend && npm run lint && npm run build && npm run test         → 127 passed, lint/build clean

Deviations

  1. No deviations from spec. The link widget kind is unchanged. The panel kind is fully replaced by chart.
  2. recharts labelFormatter type workaround. Recharts 3.x types labelFormatter as (label: ReactNode, ...) => ReactNode, not (number) => string. Wrapped with (label) => formatTime(Number(label)) to satisfy TS strict.

skill_resolution

none — no project/user SKILL.md paths were injected; no .atl/skill-registry.md found.

Residual risks

  • The chart widget assumes the Grafana datasource is Prometheus-type (hardcoded "type": "prometheus" in the query body). If the user has a non-Prometheus datasource (InfluxDB, etc.), the query body format may need adjustment. The datasource_uid is configurable but the type is not.
  • recharts is ~45KB gzipped added to the bundle.
{
  "criteriaSatisfied": [
    {
      "id": "criterion-1",
      "status": "satisfied",
      "evidence": "Replaces the broken iframe panel widget with a server-side chart query widget. Backend queries /api/ds/query with stored api_key; frontend renders recharts LineChart. No iframe, no browser auth, no CORS. The link widget kind is unchanged. 279 backend + 127 frontend tests pass; lint/build green both sides."
    }
  ],
  "changedFiles": [
    "backend/src/media_library_viewer_api/integrations/grafana.py",
    "backend/src/media_library_viewer_api/widgets/sources.py",
    "backend/tests/test_widgets.py",
    "backend/tests/test_services.py",
    "frontend/src/widgets/GrafanaChartWidget.tsx",
    "frontend/src/widgets/__tests__/GrafanaChartWidget.test.tsx",
    "frontend/src/widgets/GrafanaPanelWidget.tsx (deleted)",
    "frontend/src/widgets/__tests__/GrafanaPanelWidget.test.tsx (deleted)",
    "frontend/src/integrations/registry.ts",
    "frontend/src/integrations/registry.test.ts",
    "frontend/src/widgets/index.ts",
    "frontend/package.json"
  ],
  "testsAddedOrUpdated": [
    "backend/tests/test_widgets.py",
    "backend/tests/test_services.py",
    "frontend/src/widgets/__tests__/GrafanaChartWidget.test.tsx",
    "frontend/src/integrations/registry.test.ts"
  ],
  "commandsRun": [
    { "command": "cd backend && .venv/bin/ruff check .", "result": "passed", "summary": "All checks passed" },
    { "command": "cd backend && .venv/bin/python -m pytest tests/ -q", "result": "passed", "summary": "279 passed, 2 pre-existing warnings" },
    { "command": "cd frontend && npm run lint", "result": "passed", "summary": "0 errors, 0 warnings" },
    { "command": "cd frontend && npm run build", "result": "passed", "summary": "tsc + vite build clean" },
    { "command": "cd frontend && npm run test", "result": "passed", "summary": "39 files / 127 tests passed" }
  ],
  "validationOutput": [
    "Backend ruff clean; 279 tests pass (was 278; -2 panel + 3 chart = +1 net).",
    "Frontend eslint clean; tsc + vite build clean; 127 tests pass (-3 panel + 3 chart = net 0).",
    "GrafanaWidgetSource._fetch_chart POSTs to /api/ds/query with Bearer token; normalizes response to {series:[{label,points}]}",
    "GrafanaChartWidget renders recharts LineChart with dark-mode CSS variable colors.",
    "link widget kind unchanged; panel widget kind fully removed."
  ],
  "residualRisks": [
    "Chart query body hardcodes datasource type 'prometheus' — non-Prometheus datasources (InfluxDB etc.) may need a type field on the config.",
    "recharts adds ~45KB gzipped to the frontend bundle."
  ],
  "noStagedFiles": true,
  "diffSummary": "~400 lines: replaces Grafana panel iframe widget with server-side datasource-query chart widget. Backend: /api/ds/query POST with api_key + series normalization (70 lines). Frontend: recharts LineChart component with dark-mode support (100 lines). 3 backend + 3 frontend tests. recharts ^3.9.2 installed.",
  "reviewFindings": [
    "no blockers"
  ],
  "manualNotes": "recharts labelFormatter type workaround: recharts 3.x types it as (ReactNode) => ReactNode, not (number) => string. Wrapped with Number() cast. The link widget kind is fully preserved. The panel widget kind and all its code/tests are fully deleted."
}