Compare commits

..

1 Commits

Author SHA1 Message Date
Crowdin Bot
d4e732ef8e New Crowdin translations by GitHub Action
Some checks are pending
Lint / Linting Checks (push) Waiting to run
Tests / vitest (1) (push) Waiting to run
Tests / vitest (2) (push) Waiting to run
Tests / vitest (3) (push) Waiting to run
Tests / vitest (4) (push) Waiting to run
2026-04-01 12:32:20 +00:00
25 changed files with 52 additions and 125 deletions

View File

@@ -129,7 +129,7 @@ A progressive web app is an app that can be installed on a device and provide us
More information on PWAs can be found in [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps).
### App icons
## App icons
You can set custom icons for installable apps. More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons).
@@ -150,7 +150,7 @@ For icon `src` you can pass either full URL or a local path relative to the `/ap
### Shortcuts
Shortcuts can be used to specify links to tabs, to be preselected when the homepage is opened as an app.
Shortcuts can e used to specify links to tabs, to be preselected when the homepage is opened as an app.
More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/shortcuts).
```yaml

View File

@@ -16,7 +16,6 @@ The Glances widget allows you to monitor the resources (CPU, memory, storage, te
cpu: true # optional, enabled by default, disable by setting to false
mem: true # optional, enabled by default, disable by setting to false
cputemp: true # disabled by default
unit: imperial # optional for temp, default is metric
uptime: true # disabled by default
disk: / # disabled by default, use mount point of disk(s) in glances. Can also be a list (see below)
diskUnits: bytes # optional, bytes (default) or bbytes. Only applies to disk
@@ -32,3 +31,5 @@ disk:
- /boot
...
```
_Added in v0.4.18, updated in v0.6.11, v0.6.21_

View File

@@ -13,7 +13,7 @@ You can display general connectivity status from your Unifi (Network) Controller
An optional 'site' parameter can be supplied, if it is not the widget will use the default site for the controller.
!!! tip
!!! hint
If you enter e.g. incorrect credentials and receive an "API Error", you may need to recreate the container to clear the cache.

View File

@@ -17,7 +17,7 @@ An optional 'site' parameter can be supplied, if it is not the widget will use t
Allowed fields: `["uptime", "wan", "lan", "lan_users", "lan_devices", "wlan", "wlan_users", "wlan_devices"]` (maximum of four). Fields unsupported by the unifi device will not be shown.
!!! tip
!!! hint
If you enter e.g. incorrect credentials and receive an "API Error", you may need to recreate the container or restart the service to clear the cache.

View File

@@ -19,6 +19,6 @@ widget:
password: your_password
```
!!! tip
!!! hint
If you enter incorrect credentials and receive an "API Error", you may need to recreate the container or restart the service to clear the cache.

View File

@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.12.3",
"version": "1.10.1",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -67,9 +67,9 @@
"empty_data": "Status do Subsistema desconhecido"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "Saudável",
"degraded": "Degradado",
"no_data": "Sem dados de armazenamento disponíveis"
},
"docker": {
"rx": "RX",
@@ -114,9 +114,9 @@
},
"jellyfin": {
"playing": "Jogando",
"transcoding": "Transcoding",
"transcoding": "Transcodificando",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"no_active": "Sem Transmissões Ativas",
"movies": "Filmes",
"series": "Séries",
"episodes": "Episódios",
@@ -190,10 +190,10 @@
"plex_connection_error": "Verifique a conexão do Plex"
},
"tracearr": {
"no_active": "No Active Streams",
"streams": "Streams",
"transcodes": "Transcodes",
"directplay": "Direct Play",
"no_active": "Sem Transmissões Ativas",
"streams": "Transmissões",
"transcodes": "Transcodificações",
"directplay": "Reprodução direta",
"bitrate": "Bitrate"
},
"omada": {
@@ -619,13 +619,13 @@
"total": "Total"
},
"pangolin": {
"orgs": "Orgs",
"orgs": "Organizações",
"sites": "Sites",
"resources": "Recursos",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
"out": "Out"
"targets": "Alvos",
"traffic": "Tráfego",
"in": "Em",
"out": "Saída"
},
"peanut": {
"battery_charge": "Carga da bateria",
@@ -1164,11 +1164,11 @@
"images": "Imagens",
"image_updates": "Atualizações de Imagem",
"images_unused": "Não utilizado",
"environment_required": "Environment ID Required"
"environment_required": "ID do ambiente necessário"
},
"dockhand": {
"running": "Executando",
"stopped": "Stopped",
"stopped": "Parado",
"cpu": "CPU",
"memory": "Memória",
"images": "Imagens",
@@ -1178,12 +1178,12 @@
"stacks": "Pilhas",
"paused": "Pausado",
"total": "Total",
"environment_not_found": "Environment Not Found"
"environment_not_found": "Ambiente não encontrado"
},
"sparkyfitness": {
"eaten": "Eaten",
"burned": "Burned",
"remaining": "Remaining",
"steps": "Steps"
"eaten": "Comido",
"burned": "Queimado",
"remaining": "Restante",
"steps": "Passos"
}
}

View File

@@ -92,23 +92,6 @@ describe("pages/api/widgets/glances", () => {
expect(res.statusCode).toBe(200);
});
it("falls back to version 3 when version is invalid", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ url: "http://glances" });
httpProxy
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ total: 1 }))])
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ avg: 2 }))])
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ available: 3 }))]);
const req = { query: { index: "0", version: "3/../../secret-endpoint" } };
const res = createMockRes();
await handler(req, res);
expect(httpProxy).toHaveBeenCalledWith("http://glances/api/3/cpu", expect.any(Object));
expect(res.statusCode).toBe(200);
});
it("returns 400 when glances returns 401", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ url: "http://glances" });
httpProxy.mockResolvedValueOnce([401, null, Buffer.from("nope")]);

View File

@@ -1,6 +1,5 @@
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
import createLogger from "utils/logger";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
const logger = createLogger("glances");
@@ -46,7 +45,7 @@ export default async function handler(req, res) {
const { index, cputemp: includeCpuTemp, uptime: includeUptime, disk: includeDisks, version } = req.query;
const privateWidgetOptions = await getPrivateWidgetOptions("glances", index);
privateWidgetOptions.version = parseVersionForUrl(version, 3);
privateWidgetOptions.version = version ?? 3;
try {
const cpuData = await retrieveFromGlancesAPI(privateWidgetOptions, "cpu");

View File

@@ -10,7 +10,6 @@ import { getKubeConfig } from "utils/config/kubernetes";
import * as shvl from "utils/config/shvl";
import kubernetes from "utils/kubernetes/export";
import createLogger from "utils/logger";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
const logger = createLogger("service-helpers");
@@ -114,7 +113,7 @@ export async function servicesFromDocker() {
}
let substitutedVal = substituteEnvironmentVars(containerLabels[label]);
if (value === "widget.version" || /^widgets\[\d+\]\.version$/.test(value)) {
substitutedVal = parseVersionForUrl(substitutedVal);
substitutedVal = parseInt(substitutedVal, 10);
}
shvl.set(constructedService, value, substitutedVal);
}
@@ -591,7 +590,7 @@ export function cleanServiceGroups(groups) {
"vikunja",
].includes(type)
) {
widget.version = parseVersionForUrl(version);
if (version) widget.version = parseInt(version, 10);
}
if (type === "glances") {
if (metric) widget.metric = metric;

View File

@@ -12,22 +12,6 @@ export function formatApiCall(url, args) {
return url.replace(find, replace).replace(find, replace);
}
export function parseVersionForUrl(version, defaultValue = null) {
if (version === undefined || version === null || version === "") {
return defaultValue;
}
if (typeof version === "number") {
return Number.isInteger(version) && version >= 0 ? version : defaultValue;
}
if (typeof version === "string" && /^\d+$/.test(version)) {
return Number(version);
}
return defaultValue;
}
export function getURLSearchParams(widget, endpoint) {
const params = new URLSearchParams({
group: widget.service_group,

View File

@@ -7,7 +7,6 @@ import {
getURLSearchParams,
jsonArrayFilter,
jsonArrayTransform,
parseVersionForUrl,
sanitizeErrorURL,
} from "./api-helpers";
@@ -22,20 +21,6 @@ describe("utils/proxy/api-helpers", () => {
expect(formatApiCall("{a}-{a}-{missing}", { a: "x" })).toBe("x-x-");
});
it("parseVersionForUrl accepts canonical non-negative integers", () => {
expect(parseVersionForUrl("3")).toBe(3);
expect(parseVersionForUrl(4)).toBe(4);
expect(parseVersionForUrl(undefined, 3)).toBe(3);
});
it("parseVersionForUrl rejects non-canonical values", () => {
expect(parseVersionForUrl("3/../../path", 3)).toBe(3);
expect(parseVersionForUrl("1e2", 3)).toBe(3);
expect(parseVersionForUrl("0x10", 3)).toBe(3);
expect(parseVersionForUrl(-1, 3)).toBe(3);
expect(parseVersionForUrl(Number.NaN, 3)).toBe(3);
});
it("getURLSearchParams includes group/service/index and optionally endpoint", () => {
const widget = { service_group: "g", service_name: "s", index: "0" };

View File

@@ -2,7 +2,7 @@ import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import { asJson, formatApiCall, parseVersionForUrl } from "utils/proxy/api-helpers";
import { asJson, formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
@@ -56,7 +56,7 @@ async function getApiInfo(serviceWidget, apiName, serviceName) {
const json = asJson(data);
if (json?.data?.[apiName]) {
cgiPath = json.data[apiName].path;
maxVersion = parseVersionForUrl(json.data[apiName].maxVersion);
maxVersion = json.data[apiName].maxVersion;
logger.debug(
`Detected ${serviceWidget.type}: apiName '${apiName}', cgiPath '${cgiPath}', and maxVersion ${maxVersion}`,
);

View File

@@ -4,7 +4,6 @@ import { useTranslation } from "next-i18next";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const statusMap = {
@@ -20,12 +19,11 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const idKey = apiVersion === 3 ? "Id" : "id";
const statusKey = apiVersion === 3 ? "Status" : "status";
const idKey = version === 3 ? "Id" : "id";
const statusKey = version === 3 ? "Status" : "status";
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/containers`, {
const { data, error } = useWidgetAPI(service.widget, `${version}/containers`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});

View File

@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const Chart = dynamic(() => import("../components/chart"), { ssr: false });
@@ -17,15 +16,14 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/cpu`, {
const { data, error } = useWidgetAPI(service.widget, `${version}/cpu`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});
const { data: quicklookData, error: quicklookError } = useWidgetAPI(service.widget, `${apiVersion}/quicklook`);
const { data: quicklookData, error: quicklookError } = useWidgetAPI(service.widget, `${version}/quicklook`);
useEffect(() => {
if (data) {

View File

@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
@@ -17,7 +16,6 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [, diskName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState(
@@ -25,7 +23,7 @@ export default function Component({ service }) {
);
const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/diskio`, {
const { data, error } = useWidgetAPI(service.widget, `${version}/diskio`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});

View File

@@ -3,7 +3,6 @@ import { useTranslation } from "next-i18next";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const defaultInterval = 1000;
@@ -12,11 +11,10 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [, fsName] = widget.metric.split("fs:");
const diskUnits = widget.diskUnits === "bbytes" ? "common.bbytes" : "common.bytes";
const { data, error } = useWidgetAPI(widget, `${apiVersion}/fs`, {
const { data, error } = useWidgetAPI(widget, `${version}/fs`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});

View File

@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
@@ -17,12 +16,11 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [, gpuName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(widget, `${apiVersion}/gpu`, {
const { data, error } = useWidgetAPI(widget, `${version}/gpu`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});

View File

@@ -3,7 +3,6 @@ import { useTranslation } from "next-i18next";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
function Swap({ quicklookData, className = "" }) {
@@ -76,13 +75,12 @@ const defaultSystemInterval = 30000; // This data (OS, hostname, distribution) i
export default function Component({ service }) {
const { widget } = service;
const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, `${apiVersion}/quicklook`, {
const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, `${version}/quicklook`, {
refreshInterval,
});
const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, `${apiVersion}/system`, {
const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, `${version}/system`, {
refreshInterval: defaultSystemInterval,
});

View File

@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
@@ -18,11 +17,10 @@ export default function Component({ service }) {
const { widget } = service;
const { chart } = widget;
const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/mem`, {
const { data, error } = useWidgetAPI(service.widget, `${version}/mem`, {
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
});

View File

@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
@@ -18,16 +17,15 @@ export default function Component({ service }) {
const { widget } = service;
const { chart, metric } = widget;
const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const rxKey = apiVersion === 3 ? "rx" : "bytes_recv";
const txKey = apiVersion === 3 ? "tx" : "bytes_sent";
const rxKey = version === 3 ? "rx" : "bytes_recv";
const txKey = version === 3 ? "tx" : "bytes_sent";
const [, interfaceName] = metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(widget, `${apiVersion}/network`, {
const { data, error } = useWidgetAPI(widget, `${version}/network`, {
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
});

View File

@@ -4,7 +4,6 @@ import { useTranslation } from "next-i18next";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const statusMap = {
@@ -23,11 +22,10 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const memoryInfoKey = apiVersion === 3 ? 0 : "rss";
const memoryInfoKey = version === 3 ? 0 : "rss";
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/processlist`, {
const { data, error } = useWidgetAPI(service.widget, `${version}/processlist`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});

View File

@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
import Block from "../components/block";
import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
const Chart = dynamic(() => import("../components/chart"), { ssr: false });
@@ -17,12 +16,11 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [, sensorName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/sensors`, {
const { data, error } = useWidgetAPI(service.widget, `${version}/sensors`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});

View File

@@ -3,7 +3,7 @@ import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: credentialedProxyHandler,
allowedEndpoints: /^\d+\/(quicklook|diskio|cpu|fs|gpu|system|mem|network|processlist|sensors|containers)$/,
allowedEndpoints: /\d\/quicklook|diskio|cpu|fs|gpu|system|mem|network|processlist|sensors|containers/,
};
export default widget;

View File

@@ -8,10 +8,6 @@ describe("glances widget config", () => {
it("exports a valid widget config", () => {
expectWidgetConfigShape(widget);
expect(widget.allowedEndpoints?.test("3/quicklook")).toBe(true);
expect(widget.allowedEndpoints?.test("12/cpu")).toBe(true);
expect(widget.allowedEndpoints?.test("unknown")).toBe(false);
expect(widget.allowedEndpoints?.test("xxcpuyy")).toBe(false);
expect(widget.allowedEndpoints?.test("3/cpu/extra")).toBe(false);
expect(widget.allowedEndpoints?.test("membrane")).toBe(false);
});
});