Developer c36262d7b6 Reusable widgets: reference widgets across dashboards + detach to clone
Widgets configured on one dashboard (e.g., a Grafana service's Overview)
can now be live-referenced on other dashboards. Editing the widget config
updates it everywhere it's referenced. References can be detached into
independent clones.

Backend: new widget_references table (dashboard_scope, widget_id,
sort_order) with ON DELETE CASCADE. CRUD methods + 4 endpoints:
GET/POST /api/widgets/references, DELETE /api/widgets/references/{id},
POST /api/widgets/references/{id}/detach (clones the widget into a
standalone instance, then removes the reference).

Frontend: WidgetConfigDialog gains a dashboardScope prop. When set
(the main Dashboard passes 'main'), the dialog shows:
- Owned + referenced widgets in a combined list, with a link badge on
  references.
- 'Add existing widget' picker: searchable list of ALL widget instances
  not already on this dashboard. Click to create a reference.
- Detach button on references: clones the widget (service_id=NULL) and
  removes the reference.
- Delete on a reference removes the REFERENCE (not the original widget).

Dashboard renders referenced widgets alongside owned widgets.

Detaching a service-bound widget clones it with service_id=NULL — the
clone may need re-binding to a service to render correctly. Named
dashboards don't pass dashboardScope yet (pinned-links-only); when they
gain widget support, the backend already handles any scope string.

282 backend tests pass (+2 reference lifecycle); 127 frontend tests
pass; ruff/eslint/tsc/vite all green.
2026-07-06 11:34:48 +00:00
2026-05-04 13:50:53 +02:00
2026-06-25 10:03:54 +02:00

Manage

Manage is a media and server operations tool with Jellyfin integration, SSH file inspection, server monitoring, and safe remote job templates.

See docs/REQUIREMENTS.md for the living requirements, decisions, and planning history. See docs/MIGRATION_PLAN.md for the FastAPI + React architecture plan.

Project policy/docs:

  • License: LICENSE (MIT)
  • Contributing guide: CONTRIBUTING.md

Architecture

The project consists of two subprojects:

  • backend/ — FastAPI Python API (see backend/README.md)
  • frontend/ — React + TypeScript SPA (see frontend/README.md)
  • archive/ — Original Streamlit prototype (preserved for reference)

Features

  • Configurable dashboard with persisted widgets (Jellyfin activity, backups summary, Grafana deep-links, Prometheus metrics, Alertmanager alerts, SSH task output, static text) and shortcuts
  • Thin-dashboard observability: Alertmanager alerts, Prometheus target health, machine status, and Grafana deep-links (no in-app charting)
  • Service registry: configure Jellyfin, Jellyseerr, Alertmanager, Grafana, Prometheus, Nextcloud, and SSH task runner instances in the UI
  • Per-machine settings for SSH, monitoring targets, and file browsing
  • SQLite-indexed media table with full-library sort/filter
  • Read-only Users tab with Jellyfin as the base source and optional Jellyseerr enrichment
  • Remote file browser with ffprobe preview and job execution
  • Jellyfin API integration for library metadata and user identity data
  • SSH-based file inspection and safe remote job templates

Quick Start

Production-style deployment with the frontend serving the SPA and proxying /api to the backend. The compose files rely on environment-variable interpolation, so export the required values in your shell before running them (no env_file is needed):

docker compose up --build

Open the app at http://localhost:8080.

The production Compose file requires OIDC and Traefik variables; see Configuration below. Copy .env.example to .env, fill in the required values, and export them in your shell before running docker compose up.

Observability is external. Manage only ships its backend and frontend. It does not deploy Grafana, Prometheus, Loki, Alertmanager, Alloy, or Node Exporter. The backend exposes a /metrics endpoint and optional Alertmanager proxy endpoints so an existing observability deployment can scrape and consume them. For a ready-to-run example stack you can deploy alongside Manage, see docker-compose.observability.yml and docs/observability-runbooks.md.

Local development with hot reload:

docker compose -f docker-compose.dev.yml up --build

Frontend runs on http://localhost:5173 and the backend on http://localhost:8000. Dev compose disables OIDC by default (AUTH_ENABLED=false), so you can open it directly without an identity provider.

The backend media index and settings database (including monitoring machines, SSH keys, saved tasks, and dashboard widgets) are persisted in Docker volumes so rebuilds and container restarts do not reset state.

Manual backend/frontend development

cd backend
python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
uvicorn media_library_viewer_api.main:app --reload --port 8000
cd frontend
npm install
npm run dev

Configuration

The Compose files use environment-variable interpolation. Export the required variables in your shell or pass them inline; a .env file is optional, not required.

Compose examples

Production-style example with shell exports:

export BACKEND_APP_HOST=api.manage.example.com
export FRONTEND_APP_HOST=manage.example.com
export CERT_RESOLVER=letsencrypt
export VITE_OIDC_ISSUER=https://auth.example.com/application/o/manage/
export VITE_OIDC_CLIENT_ID=manage
export VITE_OIDC_REDIRECT_URI=https://manage.example.com/oidc/callback
export VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/
export MANAGE_ENCRYPTION_KEY=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")

docker compose up --build

Observability services (Grafana, Prometheus, Alertmanager) are configured in the app on the Services page — no env vars for them.

Inline one-liner example:

BACKEND_APP_HOST=api.manage.example.com FRONTEND_APP_HOST=manage.example.com CERT_RESOLVER=letsencrypt VITE_OIDC_ISSUER=https://auth.example.com/application/o/manage/ VITE_OIDC_CLIENT_ID=manage VITE_OIDC_REDIRECT_URI=https://manage.example.com/oidc/callback VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/ docker compose up --build

For local development, no SSH key is required unless you want to connect to remote SSH machines later:

docker compose -f docker-compose.dev.yml up --build

Example environment variables:

# Optional backend logging level
LOG_LEVEL=INFO

# Optional SMTP settings for the Users -> message popup
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_ADDRESS=no-reply@example.com
SMTP_FROM_NAME=Manage
SMTP_USE_TLS=true
SMTP_USE_SSL=false
SMTP_TIMEOUT=30

# Jellyfin, Jellyseerr, and SSH targets are now configured per machine in the app's Settings tab.
# The backend seeds a local machine automatically, so no global Jellyfin or SSH env vars are required.
#
# Remote SSH machines can store their private key and optional passphrase directly in Settings,
# so no SSH key mount is required for normal use.

# Authentik / OIDC
AUTH_ENABLED=true
OIDC_ISSUER_URL=https://auth.example.com/application/o/manage/
OIDC_AUDIENCE=manage
OIDC_JWKS_URL=
OIDC_CLOCK_SKEW_SECONDS=30

# Frontend OIDC settings
VITE_OIDC_ENABLED=true
VITE_OIDC_ISSUER=https://auth.example.com/application/o/manage/
VITE_OIDC_CLIENT_ID=manage
VITE_OIDC_SCOPE=openid profile email
VITE_OIDC_REDIRECT_URI=https://manage.example.com/oidc/callback
VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/

# Observability services (Grafana, Prometheus, Alertmanager) are configured in
# the app on the Services page. The only observability env var is the optional
# PROMETHEUS_ENABLED toggle (defaults on) for Manage's own /metrics endpoint.

# Required: master key encrypting service secrets (API keys/tokens) at rest.
# Generate one with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
MANAGE_ENCRYPTION_KEY=replace-with-a-fernet-key

Remote server requirements

The remote server needs:

  • /bin/sh (POSIX shell)
  • python3, ffprobe, find, stat, df, awk for file inspection and job templates
  • SSH access with a key configured in the app's Settings tab

The SSH client rejects unknown host keys. Connect manually once first:

ssh user@host

Development

# Backend (lint + tests)
cd backend && .venv/bin/ruff check . && .venv/bin/python -m pytest

# Frontend (lint + typecheck/build + tests)
cd frontend && npm run lint && npm run build && npm run test

Focused frontend typecheck: npx tsc --noEmit.

Notes

  • Jellyfin server root URL required (not /web). The client strips trailing /web defensively.
  • SSH commands run through /bin/sh -c regardless of remote login shell.
  • Job templates are shell-quoted. Add new templates in backend/src/media_library_viewer_api/jobs.py.
  • Root-level Docker Compose files are provided for production (docker-compose.yml) and local development (docker-compose.dev.yml), and both rely on Compose interpolation rather than env_file entries. They deploy only the backend and frontend; Manage never deploys its own observability stack (see docker-compose.observability.yml for an optional standalone example).
  • The configurable dashboard stores widget instances in the backend SQLite settings database. New installs seed default Jellyfin activity and Backups widgets automatically.
  • Grafana, Prometheus, and Alertmanager are configured as service instances in the app (Services page); their widget adapters resolve URLs from service records, and no observability URLs/credentials live in env vars. No credentials are stored in widget config; service API keys are encrypted at rest with MANAGE_ENCRYPTION_KEY. When no alertmanager service is configured, the alert proxy endpoints return graceful "not configured" responses.
S
Description
No description provided
Readme MIT 5.7 MiB
Languages
Python 57.9%
TypeScript 40.4%
JavaScript 0.8%
Shell 0.5%
CSS 0.3%
Other 0.1%