diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md
index cdad3ec..2509bcd 100644
--- a/docs/REQUIREMENTS.md
+++ b/docs/REQUIREMENTS.md
@@ -256,6 +256,54 @@ fully removed (web-ui-rework; see decision log 2026-06-17).
- Job templates should remain centralized in `jobs.py` for future extension.
- Remote job template values must be shell-quoted before execution.
+## Configurable Dashboard Widgets
+
+### Overview
+
+The dashboard is composed of persisted widget instances stored in the backend SQLite
+settings database. Each widget has a type, title, configuration, enabled flag, and
+sort order. The frontend renders enabled widgets in sort order and fetches data
+independently through the backend source adapters.
+
+### Widget types
+
+- **Jellyfin activity** — live sessions and idle users from a configured Jellyfin machine.
+- **Backups** — backup job summary and active alerts.
+- **Grafana link** — deep-link to a Grafana dashboard or panel (no iframe embedding).
+- **Prometheus metric** — result of a PromQL instant query.
+- **SSH task output** — output of a saved task run on a machine.
+- **Static text** — plain text or markdown note.
+
+### Security
+
+- Widget `config` may not contain credential keys such as `password`, `token`,
+ `secret`, `api_key`, `private_key`, or `passphrase`, or values that look like
+ secrets (e.g., base64 blobs, `sk-` prefixes).
+- Widgets reuse machine-level Jellyfin/SSH credentials and environment settings for
+ Grafana/Prometheus URLs; no secrets are stored in widget configuration.
+- SSH task widgets only run tasks from the saved-task registry; arbitrary commands
+ are not accepted.
+
+### Addon pages
+
+Each non-core addon gets a dedicated page at `/addons/:addonId`:
+
+- `/addons/grafana`
+- `/addons/prometheus`
+- `/addons/ssh-tasks`
+
+Unknown addons render a "not installed" alert.
+
+### API
+
+- `GET /api/widgets/sources` — list source types.
+- `GET /api/widgets/types` — list widget type metadata.
+- `GET /api/widgets/instances` — list widget instances.
+- `POST /api/widgets/instances` — create instance.
+- `PUT /api/widgets/instances/{id}` — update instance.
+- `DELETE /api/widgets/instances/{id}` — delete instance.
+- `GET /api/widgets/instances/{id}/data` — fetch widget data.
+
## Decision Log
- 2026-06-17: Decommissioned the legacy Manage-side system-metric scraping. Removed the backend `MonitoringPoller` (SSH-ran `df` on every machine every 5 min into a local SQLite `monitoring_machine_actions` table), the entire `services/monitoring_actions.py` module, the `/api/monitoring/poller`, `/api/monitoring/machines/{id}/actions`, and `/api/monitoring/disk` endpoints, the `monitoring_machine_actions` table (DROP on startup), the three `monitoring_poll_*` / `monitoring_action_retention_days` config knobs, and the orphaned frontend `DiskSpaceCard` + `DiskSpace` type. System metrics are now owned exclusively by Prometheus + node_exporter + Grafana. Kept the Alertmanager proxy (`/alerts`, `/alertmanager-status`, `/alertmanager-webhook`), `/prometheus-targets`, `/machines`, the `node_exporter_*` machine fields, and the on-demand `disk_usage` job template.
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index e77c6b4..ca7000e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -22,6 +22,7 @@ import { FileBrowser } from "./pages/FileBrowser";
import { Actions } from "./pages/Actions";
import BackupsPage from "./components/BackupsPage";
import { ObservabilityPage } from "./components/ObservabilityPage";
+import { AddonPage } from "./pages/AddonPage";
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
import { fetchAppVersion } from "./api/client";
import { FRONTEND_VERSION_LABEL } from "./version";
@@ -449,6 +450,7 @@ function AppInner() {
} />
} />
} />
+ } />
@@ -480,6 +482,7 @@ function AppInner() {
} />
} />
} />
+ } />
diff --git a/frontend/src/addons/GrafanaAddonPage.tsx b/frontend/src/addons/GrafanaAddonPage.tsx
new file mode 100644
index 0000000..c3adce4
--- /dev/null
+++ b/frontend/src/addons/GrafanaAddonPage.tsx
@@ -0,0 +1,37 @@
+import { ExternalLink } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+export function GrafanaAddonPage() {
+ const grafanaUrl =
+ (import.meta.env.VITE_GRAFANA_URL as string | undefined) ||
+ "http://localhost:3000";
+
+ return (
+
+
Grafana
+
+
+ Metrics & logs
+
+
+
+ Open the full Grafana instance for dashboards, metrics, and
+ log exploration.
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/addons/PrometheusAddonPage.tsx b/frontend/src/addons/PrometheusAddonPage.tsx
new file mode 100644
index 0000000..f1cd2a2
--- /dev/null
+++ b/frontend/src/addons/PrometheusAddonPage.tsx
@@ -0,0 +1,36 @@
+import { ExternalLink } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+export function PrometheusAddonPage() {
+ const prometheusUrl =
+ (import.meta.env.VITE_PROMETHEUS_URL as string | undefined) ||
+ "http://localhost:9090";
+
+ return (
+
+
Prometheus
+
+
+ Metrics explorer
+
+
+
+ Open Prometheus to run ad-hoc PromQL queries and inspect targets.
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/addons/SshTasksAddonPage.tsx b/frontend/src/addons/SshTasksAddonPage.tsx
new file mode 100644
index 0000000..f5c9de6
--- /dev/null
+++ b/frontend/src/addons/SshTasksAddonPage.tsx
@@ -0,0 +1,29 @@
+import { Terminal } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { useNavigate } from "react-router-dom";
+
+export function SshTasksAddonPage() {
+ const navigate = useNavigate();
+
+ return (
+
+
SSH tasks
+
+
+ Saved actions
+
+
+
+ Create, edit, and run saved shell or Python tasks against local
+ or remote machines.
+