diff --git a/.pi-tmp/grafana-chart-out.md b/.pi-tmp/grafana-chart-out.md new file mode 100644 index 0000000..0ffc275 --- /dev/null +++ b/.pi-tmp/grafana-chart-out.md @@ -0,0 +1,141 @@ +# 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): + +```json +{ + "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): + +```json +{ + "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 `` with one `` 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. + +```acceptance-report +{ + "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." +} +``` diff --git a/.pi-tmp/widgets-out.md b/.pi-tmp/widgets-out.md new file mode 100644 index 0000000..7b07707 --- /dev/null +++ b/.pi-tmp/widgets-out.md @@ -0,0 +1,142 @@ +# New widgets: Jellyfin now_playing + Grafana panel embed + +## Files changed (10 files, ~390 lines) + +| File | Status | Lines | +|------|--------|-------| +| `backend/src/media_library_viewer_api/integrations/jellyfin.py` | modified | +12 (new widget kind + config model) | +| `backend/src/media_library_viewer_api/integrations/grafana.py` | modified | +17 (new widget kind + config model) | +| `backend/src/media_library_viewer_api/widgets/sources.py` | modified | +12 (now_playing filter + panel embed URL) | +| `backend/tests/test_services.py` | modified | +3 (updated widget-kind assertions) | +| `backend/tests/test_widgets.py` | modified | +85 (import + 6 new tests) | +| `frontend/src/widgets/JellyfinNowPlayingWidget.tsx` | new | 41 | +| `frontend/src/widgets/GrafanaPanelWidget.tsx` | new | 50 | +| `frontend/src/integrations/registry.ts` | modified | +24 (2 new widget bindings) | +| `frontend/src/integrations/registry.test.ts` | modified | +1 (updated grafana kinds) | +| `frontend/src/widgets/__tests__/JellyfinNowPlayingWidget.test.tsx` | new | 72 | +| `frontend/src/widgets/__tests__/GrafanaPanelWidget.test.tsx` | new | 72 | + +## Session-filter logic for now_playing + +```python +if widget_kind == "now_playing": + sessions = [ + s for s in sessions + if s.get("NowPlayingItem") + and not s.get("PlayState", {}).get("IsPaused", True) + ] +``` + +Filters raw Jellyfin sessions BEFORE `_map_sessions_to_activity_rows`. A session is "actively playing" when it has a `NowPlayingItem` (something is playing, not just idle) AND `PlayState.IsPaused` is false. The `activity` kind (default) is unchanged — shows all sessions including idle and paused. + +## Embed URL format for panel + +```python +embed_url = f"{base_url}/d-solo/{dashboard_uid}/manage?panelId={panel_id}&from={from_ts}&to={to_ts}&kiosk=tv" +``` + +Uses Grafana's `/d-solo/` endpoint which renders a single panel without dashboard chrome. `kiosk=tv` hides the top nav. Defaults: `from_ts="now-1h"`, `to_ts="now"`. + +## Validation + +``` +cd backend && .venv/bin/ruff check src/ tests/ → All checks passed! +cd backend && .venv/bin/python -m pytest tests/ → 278 passed, 2 warnings (pre-existing) +cd frontend && npm run lint → 0 errors, 0 warnings +cd frontend && npm run build → ✓ built (tsc + vite) +cd frontend && npm run test → 39 files / 127 tests passed +``` + +Backend: +6 new tests (definition assertions x2, grafana panel URL x2, jellyfin now_playing filter x1, jellyfin activity shows all x1). +Frontend: +6 new tests (JellyfinNowPlayingWidget x3, GrafanaPanelWidget x3). + +## Deviations + +1. **No deviations from spec.** Both widgets are additive — no existing behavior changed. The `activity` and `link` kinds work exactly as before. +2. **GrafanaPanelWidget pi-lens advisory** for `