diff --git a/public/icons/services/backrest-dark.svg b/public/icons/services/backrest-dark.svg
new file mode 100644
index 0000000..5ff59f8
--- /dev/null
+++ b/public/icons/services/backrest-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/services/backrest-light.svg b/public/icons/services/backrest-light.svg
new file mode 100644
index 0000000..15ea945
--- /dev/null
+++ b/public/icons/services/backrest-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/services/proxmox-dark.svg b/public/icons/services/proxmox-dark.svg
new file mode 100644
index 0000000..af63010
--- /dev/null
+++ b/public/icons/services/proxmox-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/services/proxmox-light.svg b/public/icons/services/proxmox-light.svg
new file mode 100644
index 0000000..3c0eb19
--- /dev/null
+++ b/public/icons/services/proxmox-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/content/docs/docs/create-a-widget/data.md b/src/content/docs/docs/create-a-widget/data.md
index 32802e7..cd955ab 100644
--- a/src/content/docs/docs/create-a-widget/data.md
+++ b/src/content/docs/docs/create-a-widget/data.md
@@ -29,7 +29,7 @@ A widget that renders entirely in the browser, like [Clock](/docs/widgets/clock/
| `ctx.endpoint` | The endpoint name, set for an `optionsFrom` fetch or a multi-view widget. |
| `ctx.row` | For an `optionsFrom` fetch from a field in a `group`, that row's values. Otherwise `null`. |
| `ctx.params` | The query parameters the manifest lists in [`params`](/docs/create-a-widget/manifest/#query-parameters), plus `endpoint`, as `URLSearchParams`. |
-| `ctx.fetchJSON(url, opts)` | Fetches a URL and parses the body. Returns `{ status, data }` or throws. JSON is returned as-is. Prometheus text and XML are parsed. Pass `{ raw: true }` for the untouched text. |
+| `ctx.fetchJSON(url, opts)` | Fetches a URL and parses the body. Returns `{ status, data }` or throws. JSON is returned as-is. Prometheus text and XML are parsed. Pass `{ raw: true }` for the untouched text. At most six requests to one service run at once. The rest wait in order. |
| `ctx.parsePrometheus(text)` | Parses a Prometheus metrics body. Non-string input gives an empty object. |
| `ctx.normalizeBase(raw)` | Tidies a user-entered base URL: adds a scheme, drops a trailing slash. |
| `ctx.metrics` | Host metrics: `cpuSample`, `ramPercent`, `cpuTemp`, `diskStats`, `procCount`, `uptimeSeconds`. Each is a function. `cpuSample()` is async and returns `{ cpu, iowait }`. They read `/proc` and `/sys`, so they report the host, not the container limits. |
diff --git a/src/content/docs/docs/create-a-widget/manifest.md b/src/content/docs/docs/create-a-widget/manifest.md
index 778c503..9d4eadd 100644
--- a/src/content/docs/docs/create-a-widget/manifest.md
+++ b/src/content/docs/docs/create-a-widget/manifest.md
@@ -177,7 +177,7 @@ That saves `{ "vpn": { "url": "...", "apiKey": "..." } }`. A sub-field `showIf`
## Options from the service
-When a `select` can only be filled after the user enters a URL and key, give it `"optionsFrom": ""`. The form shows a **Fetch** button, which calls `data.js` with `ctx.endpoint` set to that name. Return `{ options: [{ value, label }, ...] }`.
+When a `select` can only be filled after the user enters a URL and key, give it `"optionsFrom": ""`. The form shows a **Fetch** button, which calls `data.js` with `ctx.endpoint` set to that name. Return `{ options: [{ value, label }, ...] }`. If the service listed more than you return, add `truncated: true`. The form then says the list was cut short.
The fetch receives the current form values, `transient` fields included.
diff --git a/src/content/docs/docs/widgets/backup.mdx b/src/content/docs/docs/widgets/backup.mdx
index 344d186..778c920 100644
--- a/src/content/docs/docs/widgets/backup.mdx
+++ b/src/content/docs/docs/widgets/backup.mdx
@@ -1,6 +1,6 @@
---
title: Backup widget
-description: Last run, next run and live status of Duplicati and Kopia backup jobs on your Stackyard dashboard.
+description: Last run, next run and live status of Duplicati, Kopia, Proxmox Backup Server and Backrest backup jobs on your Stackyard dashboard.
---
import WidgetGroup from '../../../../components/WidgetGroup.astro';
@@ -42,10 +42,51 @@ Small holds one slot. Medium holds three. Each slot reads one job.
],
links: [{ label: 'kopia/kopia', href: 'https://github.com/kopia/kopia' }],
},
+ {
+ name: 'Proxmox Backup Server', icon: 'proxmox',
+ kind: 'Backup server',
+ needs: 'URL, API token',
+ fields: [
+ ['URL', 'The server address, for example `https://pbs.local:8007`.'],
+ ['Token ID', 'The API token, for example `stackyard@pbs!dashboard`.'],
+ ['Token Secret', 'Stored as a secret.'],
+ ['Allow Self-Signed Certificate', 'Turn on if the server uses its default certificate.'],
+ ['Backup Group', 'Optional. Enter the URL and token, then load the groups and pick one. Lists up to 20 datastores, and up to 20 namespaces in each.'],
+ ],
+ links: [{ label: 'Proxmox Backup Server', href: 'https://www.proxmox.com/en/products/proxmox-backup-server/overview' }],
+ },
+ {
+ name: 'Backrest', icon: 'backrest',
+ kind: 'Backup software',
+ needs: 'URL, login if set',
+ fields: [
+ ['URL', 'The Backrest address, for example `http://backrest:9898`.'],
+ ['Username', 'Optional. Leave blank if sign-in is off.'],
+ ['Password', 'Optional. Stored as a secret.'],
+ ['Plan', 'Optional. Enter the URL and credentials, then load the plans and pick one.'],
+ ],
+ links: [{ label: 'garethgeorge/backrest', href: 'https://github.com/garethgeorge/backrest' }],
+ },
]} />
+
+
+## Proxmox Backup Server
+
+A slot reads one backup group, such as `vm/100`. The card is named from the newest backup's notes, else the group.
+
+- Give the token the Audit role. On the datastore, the card shows the last backup, and failed if the newest backup failed verification. On `/`, it also shows running and failed backup tasks.
+- A backup older than 25 hours shows a warning.
+- Proxmox Backup Server does not hold the backup schedule, so the card shows no next run.
+- A group moved to another namespace of its datastore is found again when exactly one namespace holds it. Pick the group again if two do.
+
+## Backrest
+
+A slot reads one plan. The card shows the last run, the next run, and missed when Backrest marks the plan overdue. Missed needs Backrest 1.14 or newer.
+
+Backrest has no read-only user. Any Backrest login has full control of Backrest. Stackyard only reads from it.
diff --git a/src/content/docs/docs/widgets/index.mdx b/src/content/docs/docs/widgets/index.mdx
index 3b3bf3f..c1a0199 100644
--- a/src/content/docs/docs/widgets/index.mdx
+++ b/src/content/docs/docs/widgets/index.mdx
@@ -22,7 +22,7 @@ Widgets reorder in the same list as apps and folders. See [Apps and folders](/do
## The widgets