mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-06 21:57:48 +01:00
Compare commits
11 Commits
f4efc71350
...
547ef0c4c5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
547ef0c4c5 | ||
|
|
11d148fff0 | ||
|
|
eb61d69626 | ||
|
|
876304cda5 | ||
|
|
65dce6d387 | ||
|
|
7b60a60d4e | ||
|
|
8d37cad871 | ||
|
|
cd25ae3258 | ||
|
|
a27cdbc284 | ||
|
|
e772ef0ad1 | ||
|
|
8cc00ae09a |
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
6
.github/workflows/docs-publish.yml
vendored
6
.github/workflows/docs-publish.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
- name: Check files
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: echo "cache_id=${{github.sha}}" >> $GITHUB_ENV
|
||||
|
||||
8
.github/workflows/repo-maintenance.yml
vendored
8
.github/workflows/repo-maintenance.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
name: 'Stale'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-stale: 7
|
||||
days-before-close: 14
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
name: 'Close Answered Discussions'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
function sleep(ms) {
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
name: 'Close Outdated Discussions'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
function sleep(ms) {
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
name: 'Close Unsupported Feature Requests'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
function sleep(ms) {
|
||||
|
||||
@@ -4,11 +4,13 @@ description: Proxmox Configuration
|
||||
---
|
||||
|
||||
The Proxmox connection is configured in the `proxmox.yaml` file. See [Create token](#create-token) section below for details on how to generate the required API token.
|
||||
You can configure multiple nodes - be sure to use the exact `proxmoxNode` identifier!
|
||||
|
||||
```yaml
|
||||
url: https://proxmox.host.or.ip:8006
|
||||
token: username@pam!Token ID
|
||||
secret: secret
|
||||
pve:
|
||||
url: https://proxmox.host.or.ip:8006
|
||||
token: username@pam!Token ID
|
||||
secret: secret
|
||||
```
|
||||
|
||||
## Services
|
||||
@@ -17,7 +19,7 @@ Once the Proxmox connection is configured, individual services can be configured
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- `proxmoxNode`: The name of the Proxmox node where your VM/LXC is running
|
||||
- `proxmoxNode`: The name of the Proxmox node where your VM/LXC is running, must match with a node configured in the `proxmox.yaml`
|
||||
- `proxmoxVMID`: The ID of the Proxmox VM or LXC container
|
||||
- `proxmoxType`: (Optional) The type of Proxmox virtual machine. Defaults to `qemu` for VMs, but can be set to `lxc` for LXC containers
|
||||
|
||||
|
||||
17
docs/widgets/services/backrest.md
Normal file
17
docs/widgets/services/backrest.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Backrest
|
||||
description: Backrest Widget Configuration
|
||||
---
|
||||
|
||||
[Backrest](https://garethgeorge.github.io/backrest/) is a web-based frontend for
|
||||
the [Restic](https://restic.net/) backup tool.
|
||||
|
||||
**Allowed fields:** `["num_success_latest","num_failure_latest","num_success_30","num_plans","num_failure_30","bytes_added_30"]`
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: backrest
|
||||
url: http://backrest.host.or.ip
|
||||
username: admin # optional if auth is enabled in Backrest
|
||||
password: admin # optional if auth is enabled in Backrest
|
||||
```
|
||||
@@ -15,6 +15,7 @@ You can also find a list of all available service widgets in the sidebar navigat
|
||||
- [Authentik](authentik.md)
|
||||
- [Autobrr](autobrr.md)
|
||||
- [Azure DevOps](azuredevops.md)
|
||||
- [Backrest](backrest.md)
|
||||
- [Bazarr](bazarr.md)
|
||||
- [Beszel](beszel.md)
|
||||
- [Caddy](caddy.md)
|
||||
|
||||
@@ -39,6 +39,7 @@ nav:
|
||||
- widgets/services/authentik.md
|
||||
- widgets/services/autobrr.md
|
||||
- widgets/services/azuredevops.md
|
||||
- widgets/services/backrest.md
|
||||
- widgets/services/bazarr.md
|
||||
- widgets/services/beszel.md
|
||||
- widgets/services/caddy.md
|
||||
|
||||
10
package.json
10
package.json
@@ -11,13 +11,13 @@
|
||||
"telemetry": "next telemetry disable"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@kubernetes/client-node": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"dockerode": "^4.0.7",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"gamedig": "^5.2.0",
|
||||
"gamedig": "^5.3.1",
|
||||
"i18next": "^24.2.3",
|
||||
"ical.js": "^2.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -25,7 +25,7 @@
|
||||
"luxon": "^3.6.1",
|
||||
"memory-cache": "^0.2.0",
|
||||
"minecraftstatuspinger": "^1.2.2",
|
||||
"next": "^15.4.5",
|
||||
"next": "^15.5.2",
|
||||
"next-i18next": "^12.1.0",
|
||||
"ping": "^0.4.4",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
@@ -34,10 +34,10 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-icons": "^5.4.0",
|
||||
"recharts": "^2.15.3",
|
||||
"recharts": "^3.1.2",
|
||||
"swr": "^2.3.3",
|
||||
"systeminformation": "^5.27.7",
|
||||
"tough-cookie": "^5.1.2",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"urbackup-server-api": "^0.8.9",
|
||||
"winston": "^3.17.0",
|
||||
"xml-js": "^1.6.11"
|
||||
|
||||
679
pnpm-lock.yaml
generated
679
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1106,5 +1106,13 @@
|
||||
"arrayFree": "Array Free",
|
||||
"poolUsed": "{{pool}} Used",
|
||||
"poolFree": "{{pool}} Free"
|
||||
},
|
||||
"backrest": {
|
||||
"num_plans": "Plans",
|
||||
"num_success_30": "Successes",
|
||||
"num_failure_30": "Failures",
|
||||
"num_success_latest": "Succeeding",
|
||||
"num_failure_latest": "Failing",
|
||||
"bytes_added_30": "Bytes Added"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,28 @@ export default async function handler(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = `${proxmoxConfig.url}/api2/json`;
|
||||
// Prefer per-node config (new format), fall back to legacy flat creds.
|
||||
const nodeConfig =
|
||||
(node && proxmoxConfig && proxmoxConfig[node]) ||
|
||||
(proxmoxConfig && proxmoxConfig.url && proxmoxConfig.token && proxmoxConfig.secret
|
||||
? {
|
||||
url: proxmoxConfig.url,
|
||||
token: proxmoxConfig.token,
|
||||
secret: proxmoxConfig.secret,
|
||||
}
|
||||
: null);
|
||||
|
||||
if (!nodeConfig) {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
"Proxmox config not found for the specified node and no legacy credentials detected. " +
|
||||
"Add a node block in proxmox.yaml (e.g., 'pve: { url, token, secret }') or restore legacy top-level url/token/secret.",
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = `${nodeConfig.url}/api2/json`;
|
||||
const headers = {
|
||||
Authorization: `PVEAPIToken=${proxmoxConfig.token}=${proxmoxConfig.secret}`,
|
||||
Authorization: `PVEAPIToken=${nodeConfig.token}=${nodeConfig.secret}`,
|
||||
};
|
||||
|
||||
const statusUrl = `${baseUrl}/nodes/${node}/${vmType}/${vmid}/status/current`;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
# url: https://proxmox.host.or.ip:8006
|
||||
# token: username@pam!Token ID
|
||||
# secret: secret
|
||||
# pve:
|
||||
# url: https://proxmox.host.or.ip:8006
|
||||
# token: username@pam!Token ID
|
||||
# secret: secret
|
||||
|
||||
50
src/widgets/backrest/component.jsx
Normal file
50
src/widgets/backrest/component.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
const BACKREST_DEFAULT_FIELDS = ["num_success_latest", "num_failure_latest", "num_failure_30", "bytes_added_30"];
|
||||
const MAX_ALLOWED_FIELDS = 4;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, "summary");
|
||||
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (!widget.fields?.length) {
|
||||
widget.fields = BACKREST_DEFAULT_FIELDS;
|
||||
} else if (widget.fields.length > MAX_ALLOWED_FIELDS) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="backrest.num_plans" />
|
||||
<Block label="backrest.num_success_latest" />
|
||||
<Block label="backrest.num_failure_latest" />
|
||||
<Block label="backrest.num_success_30" />
|
||||
<Block label="backrest.num_failure_30" />
|
||||
<Block label="backrest.bytes_added_30" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="backrest.num_plans" value={t("common.number", { value: data.numPlans })} />
|
||||
<Block label="backrest.num_success_latest" value={t("common.number", { value: data.numSuccessLatest })} />
|
||||
<Block label="backrest.num_failure_latest" value={t("common.number", { value: data.numFailureLatest })} />
|
||||
<Block label="backrest.num_success_30" value={t("common.number", { value: data.numSuccess30Days })} />
|
||||
<Block label="backrest.num_failure_30" value={t("common.number", { value: data.numFailure30Days })} />
|
||||
<Block label="backrest.bytes_added_30" value={t("common.bytes", { value: data.bytesAdded30Days })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
96
src/widgets/backrest/proxy.js
Normal file
96
src/widgets/backrest/proxy.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import createLogger from "utils/logger";
|
||||
import { asJson, formatApiCall } from "utils/proxy/api-helpers";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import widgets from "widgets/widgets";
|
||||
|
||||
const proxyName = "backrestProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
function sumField(plans, field) {
|
||||
return plans.reduce((sum, plan) => {
|
||||
const num = Number(plan[field]);
|
||||
return sum + (Number.isNaN(num) ? 0 : num);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function buildResponse(plans) {
|
||||
const numSuccess30Days = sumField(plans, "backupsSuccessLast30days");
|
||||
const numFailure30Days = sumField(plans, "backupsFailed30days");
|
||||
const bytesAdded30Days = sumField(plans, "bytesAddedLast30days");
|
||||
|
||||
var numSuccessLatest = 0;
|
||||
var numFailureLatest = 0;
|
||||
|
||||
plans.forEach((plan) => {
|
||||
const statuses = plan?.recentBackups?.status;
|
||||
if (Array.isArray(statuses) && statuses.length > 0) {
|
||||
if (statuses[0] === "STATUS_SUCCESS") {
|
||||
numSuccessLatest++;
|
||||
} else {
|
||||
numFailureLatest++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
numPlans: plans.length,
|
||||
numSuccess30Days,
|
||||
numFailure30Days,
|
||||
numSuccessLatest,
|
||||
numFailureLatest,
|
||||
bytesAdded30Days,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function backrestProxyHandler(req, res) {
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
|
||||
if (widget.username && widget.password) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
|
||||
}
|
||||
|
||||
const { api } = widgets[widget.type];
|
||||
const url = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||
|
||||
try {
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
headers,
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("Error getting data from Backrest: %d. Data: %s", status, data);
|
||||
return res.status(500).send({ error: { message: "Error getting data from Backrest", url, data } });
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", "application/json");
|
||||
const plans = asJson(data).planSummaries;
|
||||
if (!Array.isArray(plans)) {
|
||||
logger.error("Invalid plans data: %s", JSON.stringify(plans));
|
||||
return res.status(500).send({ error: { message: "Invalid plans data", url, data } });
|
||||
}
|
||||
const response = buildResponse(plans);
|
||||
return res.status(status).send(response);
|
||||
} catch (error) {
|
||||
logger.error("Exception calling Backrest API: %s", error.message);
|
||||
return res.status(500).json({ error: "Backrest API Error", message: error.message });
|
||||
}
|
||||
}
|
||||
14
src/widgets/backrest/widget.js
Normal file
14
src/widgets/backrest/widget.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import backrestProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/v1.Backrest/{endpoint}",
|
||||
proxyHandler: backrestProxyHandler,
|
||||
|
||||
mappings: {
|
||||
summary: {
|
||||
endpoint: "GetSummaryDashboard",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -9,6 +9,7 @@ const components = {
|
||||
authentik: dynamic(() => import("./authentik/component")),
|
||||
autobrr: dynamic(() => import("./autobrr/component")),
|
||||
azuredevops: dynamic(() => import("./azuredevops/component")),
|
||||
backrest: dynamic(() => import("./backrest/component")),
|
||||
bazarr: dynamic(() => import("./bazarr/component")),
|
||||
beszel: dynamic(() => import("./beszel/component")),
|
||||
caddy: dynamic(() => import("./caddy/component")),
|
||||
|
||||
@@ -6,6 +6,7 @@ import audiobookshelf from "./audiobookshelf/widget";
|
||||
import authentik from "./authentik/widget";
|
||||
import autobrr from "./autobrr/widget";
|
||||
import azuredevops from "./azuredevops/widget";
|
||||
import backrest from "./backrest/widget";
|
||||
import bazarr from "./bazarr/widget";
|
||||
import beszel from "./beszel/widget";
|
||||
import caddy from "./caddy/widget";
|
||||
@@ -151,6 +152,7 @@ const widgets = {
|
||||
authentik,
|
||||
autobrr,
|
||||
azuredevops,
|
||||
backrest,
|
||||
bazarr,
|
||||
beszel,
|
||||
caddy,
|
||||
|
||||
Reference in New Issue
Block a user