mirror of
https://github.com/gethomepage/homepage.git
synced 2025-09-29 15:31:09 -04:00
Feature: Backrest widget (#5741)
Co-authored-by: Renan Greca <renangreca@gmail.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
parent
8d37cad871
commit
7b60a60d4e
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)
|
- [Authentik](authentik.md)
|
||||||
- [Autobrr](autobrr.md)
|
- [Autobrr](autobrr.md)
|
||||||
- [Azure DevOps](azuredevops.md)
|
- [Azure DevOps](azuredevops.md)
|
||||||
|
- [Backrest](backrest.md)
|
||||||
- [Bazarr](bazarr.md)
|
- [Bazarr](bazarr.md)
|
||||||
- [Beszel](beszel.md)
|
- [Beszel](beszel.md)
|
||||||
- [Caddy](caddy.md)
|
- [Caddy](caddy.md)
|
||||||
|
@ -39,6 +39,7 @@ nav:
|
|||||||
- widgets/services/authentik.md
|
- widgets/services/authentik.md
|
||||||
- widgets/services/autobrr.md
|
- widgets/services/autobrr.md
|
||||||
- widgets/services/azuredevops.md
|
- widgets/services/azuredevops.md
|
||||||
|
- widgets/services/backrest.md
|
||||||
- widgets/services/bazarr.md
|
- widgets/services/bazarr.md
|
||||||
- widgets/services/beszel.md
|
- widgets/services/beszel.md
|
||||||
- widgets/services/caddy.md
|
- widgets/services/caddy.md
|
||||||
|
@ -1106,5 +1106,13 @@
|
|||||||
"arrayFree": "Array Free",
|
"arrayFree": "Array Free",
|
||||||
"poolUsed": "{{pool}} Used",
|
"poolUsed": "{{pool}} Used",
|
||||||
"poolFree": "{{pool}} Free"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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")),
|
authentik: dynamic(() => import("./authentik/component")),
|
||||||
autobrr: dynamic(() => import("./autobrr/component")),
|
autobrr: dynamic(() => import("./autobrr/component")),
|
||||||
azuredevops: dynamic(() => import("./azuredevops/component")),
|
azuredevops: dynamic(() => import("./azuredevops/component")),
|
||||||
|
backrest: dynamic(() => import("./backrest/component")),
|
||||||
bazarr: dynamic(() => import("./bazarr/component")),
|
bazarr: dynamic(() => import("./bazarr/component")),
|
||||||
beszel: dynamic(() => import("./beszel/component")),
|
beszel: dynamic(() => import("./beszel/component")),
|
||||||
caddy: dynamic(() => import("./caddy/component")),
|
caddy: dynamic(() => import("./caddy/component")),
|
||||||
|
@ -6,6 +6,7 @@ import audiobookshelf from "./audiobookshelf/widget";
|
|||||||
import authentik from "./authentik/widget";
|
import authentik from "./authentik/widget";
|
||||||
import autobrr from "./autobrr/widget";
|
import autobrr from "./autobrr/widget";
|
||||||
import azuredevops from "./azuredevops/widget";
|
import azuredevops from "./azuredevops/widget";
|
||||||
|
import backrest from "./backrest/widget";
|
||||||
import bazarr from "./bazarr/widget";
|
import bazarr from "./bazarr/widget";
|
||||||
import beszel from "./beszel/widget";
|
import beszel from "./beszel/widget";
|
||||||
import caddy from "./caddy/widget";
|
import caddy from "./caddy/widget";
|
||||||
@ -151,6 +152,7 @@ const widgets = {
|
|||||||
authentik,
|
authentik,
|
||||||
autobrr,
|
autobrr,
|
||||||
azuredevops,
|
azuredevops,
|
||||||
|
backrest,
|
||||||
bazarr,
|
bazarr,
|
||||||
beszel,
|
beszel,
|
||||||
caddy,
|
caddy,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user