Compare commits

..
1 Commits
Author SHA1 Message Date
Crowdin Bot a6fcde2222 New Crowdin translations by GitHub Action
Lint / Linting Checks (push) Canceled after 0s
Tests / vitest (1) (push) Canceled after 0s
Tests / vitest (2) (push) Canceled after 0s
Tests / vitest (3) (push) Canceled after 0s
Tests / vitest (4) (push) Canceled after 0s
2026-10-13 12:32:56 +00:00
13 changed files with 59 additions and 121 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ jobs:
run_install: false run_install: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with: with:
node-version: 24 node-version: 24
cache: 'pnpm' cache: 'pnpm'
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
run_install: false run_install: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with: with:
node-version: 24 node-version: 24
cache: 'pnpm' cache: 'pnpm'
+3 -3
View File
@@ -26,14 +26,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' - if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != ''
uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7 uses: release-drafter/release-drafter@c345907820e0396f6f44bcfd9ac4763325f8feb0 # v7
with: with:
config-name: release-drafter.yml config-name: release-drafter.yml
version: ${{ github.event.inputs.version }} version: ${{ github.event.inputs.version }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == '' - if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == ''
uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7 uses: release-drafter/release-drafter@c345907820e0396f6f44bcfd9ac4763325f8feb0 # v7
with: with:
config-name: release-drafter.yml config-name: release-drafter.yml
env: env:
@@ -47,7 +47,7 @@ jobs:
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: release-drafter/release-drafter/autolabeler@978bb0b30c5ee8cdd332ced253758157150d43f0 - uses: release-drafter/release-drafter/autolabeler@c345907820e0396f6f44bcfd9ac4763325f8feb0
with: with:
config-name: release-drafter.yml config-name: release-drafter.yml
env: env:
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
with: with:
version: 9 version: 9
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
+1 -1
View File
@@ -791,7 +791,7 @@
"processing": "Processant", "processing": "Processant",
"processed": "Processat", "processed": "Processat",
"time": "Temps", "time": "Temps",
"failed": "Failed" "failed": "Error"
}, },
"firefly": { "firefly": {
"networth": "Valor Net", "networth": "Valor Net",
+15 -15
View File
@@ -871,12 +871,12 @@
"finished": "Fertig" "finished": "Fertig"
}, },
"bookorbit": { "bookorbit": {
"books": "Books", "books": "Bücher",
"authors": "Authors", "authors": "Autoren",
"series": "Series", "series": "Serien",
"storage": "Storage", "storage": "Storage",
"reading": "Reading", "reading": "Reading",
"finished": "Finished" "finished": "Beendet"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "Warteschlange", "downloadCount": "Warteschlange",
@@ -999,11 +999,11 @@
"message": "Nachricht", "message": "Nachricht",
"tags": "Schlagwörter", "tags": "Schlagwörter",
"none": "Keine", "none": "Keine",
"min": "Min", "min": "Minimal",
"low": "Low", "low": "Niedrig",
"default": "Default", "default": "Standard",
"high": "High", "high": "Hoch",
"urgent": "Urgent" "urgent": "Dringend"
}, },
"plantit": { "plantit": {
"events": "Ereignisse", "events": "Ereignisse",
@@ -1117,8 +1117,8 @@
"beszel": { "beszel": {
"name": "Name", "name": "Name",
"systems": "Systeme", "systems": "Systeme",
"up": "Up", "up": "Online",
"down": "Down", "down": "Offline",
"paused": "Pausiert", "paused": "Pausiert",
"pending": "Wartend", "pending": "Wartend",
"status": "Status", "status": "Status",
@@ -1268,10 +1268,10 @@
"steps": "Schritte" "steps": "Schritte"
}, },
"syncthing": { "syncthing": {
"connected": "Connected", "connected": "Verbunden",
"synced": "Synced", "synced": "Synchronisiert",
"errors": "Errors", "errors": "Fehler",
"storage": "Storage" "storage": "Speicher"
}, },
"sportarr": { "sportarr": {
"wanted": "Wanted", "wanted": "Wanted",
+13 -13
View File
@@ -257,14 +257,14 @@
"seed": "Semillas" "seed": "Semillas"
}, },
"qui": { "qui": {
"leech": "Leech", "leech": "Sanguijuela",
"download": "Download", "download": "Descargar",
"seed": "Seed", "seed": "Semilla",
"upload": "Upload", "upload": "Subir",
"total": "Total", "total": "Total",
"errored": "Errored", "errored": "Con error",
"ratio": "Ratio", "ratio": "Ratio",
"freeSpace": "Free" "freeSpace": "Libre"
}, },
"qnap": { "qnap": {
"cpuUsage": "Uso de la CPU", "cpuUsage": "Uso de la CPU",
@@ -791,7 +791,7 @@
"processing": "Procesando", "processing": "Procesando",
"processed": "Procesado", "processed": "Procesado",
"time": "Tiempo", "time": "Tiempo",
"failed": "Failed" "failed": "Fallido"
}, },
"firefly": { "firefly": {
"networth": "Patrimonio neto", "networth": "Patrimonio neto",
@@ -871,12 +871,12 @@
"finished": "Finalizado" "finished": "Finalizado"
}, },
"bookorbit": { "bookorbit": {
"books": "Books", "books": "Libros",
"authors": "Authors", "authors": "Autores",
"series": "Series", "series": "Series",
"storage": "Storage", "storage": "Almacenamiento",
"reading": "Reading", "reading": "Lectura",
"finished": "Finished" "finished": "Finalizado"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "En cola", "downloadCount": "En cola",
@@ -1279,6 +1279,6 @@
"leagues": "Ligas" "leagues": "Ligas"
}, },
"feed": { "feed": {
"noItems": "No items" "noItems": "No hay elementos"
} }
} }
+16 -16
View File
@@ -257,14 +257,14 @@
"seed": "Сид" "seed": "Сид"
}, },
"qui": { "qui": {
"leech": "Leech", "leech": "Лич",
"download": "Download", "download": "Скачивание",
"seed": "Seed", "seed": "Сид",
"upload": "Upload", "upload": "Загрузка",
"total": "Total", "total": "Всего",
"errored": "Errored", "errored": "Ошибок",
"ratio": "Ratio", "ratio": "Соотношение",
"freeSpace": "Free" "freeSpace": "Свободно"
}, },
"qnap": { "qnap": {
"cpuUsage": "Использование ЦП", "cpuUsage": "Использование ЦП",
@@ -791,7 +791,7 @@
"processing": "Обрабатывается", "processing": "Обрабатывается",
"processed": "Обработано", "processed": "Обработано",
"time": "Время", "time": "Время",
"failed": "Failed" "failed": "Неудачно"
}, },
"firefly": { "firefly": {
"networth": "Общая средства", "networth": "Общая средства",
@@ -871,12 +871,12 @@
"finished": "Завершено" "finished": "Завершено"
}, },
"bookorbit": { "bookorbit": {
"books": "Books", "books": "Книги",
"authors": "Authors", "authors": "Авторы",
"series": "Series", "series": "Серии",
"storage": "Storage", "storage": "Память",
"reading": "Reading", "reading": "Чтение",
"finished": "Finished" "finished": "Завершено"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "Очередь", "downloadCount": "Очередь",
@@ -1279,6 +1279,6 @@
"leagues": "Лиги" "leagues": "Лиги"
}, },
"feed": { "feed": {
"noItems": "No items" "noItems": "Ничего нет"
} }
} }
+2 -2
View File
@@ -69,11 +69,11 @@ export function substituteEnvironmentVars(str) {
const cachedVars = getCachedEnvironmentVars(); const cachedVars = getCachedEnvironmentVars();
cachedVars.forEach(([key, value]) => { cachedVars.forEach(([key, value]) => {
if (key.startsWith(homepageVarPrefix)) { if (key.startsWith(homepageVarPrefix)) {
result = result.replaceAll(`{{${key}}}`, () => value); result = result.replaceAll(`{{${key}}}`, value);
} else if (key.startsWith(homepageFilePrefix)) { } else if (key.startsWith(homepageFilePrefix)) {
const filename = value; const filename = value;
const fileContents = readFileSync(filename, "utf8"); const fileContents = readFileSync(filename, "utf8");
result = result.replaceAll(`{{${key}}}`, () => fileContents); result = result.replaceAll(`{{${key}}}`, fileContents);
} }
}); });
} }
+1 -34
View File
@@ -1,4 +1,4 @@
import { mkdtempSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs"; import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import path from "node:path"; import path from "node:path";
@@ -37,39 +37,6 @@ describe("utils/config/config", () => {
expect(mod.substituteEnvironmentVars("token={{HOMEPAGE_FILE_SECRET}}")).toBe("token=secret"); expect(mod.substituteEnvironmentVars("token={{HOMEPAGE_FILE_SECRET}}")).toBe("token=secret");
}); });
it.each(["pa$$word", "pa$&word", "pa$`word", "pa$'word", "pa$1word"])(
"substituteEnvironmentVars preserves literal environment value %j in every occurrence",
async (secret) => {
process.env.HOMEPAGE_VAR_SECRET = secret;
const mod = await import("./config");
expect(
mod.substituteEnvironmentVars("before {{HOMEPAGE_VAR_SECRET}} between {{HOMEPAGE_VAR_SECRET}} after"),
).toBe(`before ${secret} between ${secret} after`);
},
);
it.each(["pa$$word", "pa$&word", "pa$`word", "pa$'word", "pa$1word"])(
"substituteEnvironmentVars preserves literal secret file contents %j in every occurrence",
async (secret) => {
const dir = mkdtempSync(path.join(tmpdir(), "homepage-config-test-"));
const secretPath = path.join(dir, "secret.txt");
writeFileSync(secretPath, secret, "utf8");
try {
process.env.HOMEPAGE_FILE_SECRET = secretPath;
const mod = await import("./config");
expect(
mod.substituteEnvironmentVars("before {{HOMEPAGE_FILE_SECRET}} between {{HOMEPAGE_FILE_SECRET}} after"),
).toBe(`before ${secret} between ${secret} after`);
} finally {
unlinkSync(secretPath);
rmdirSync(dir);
}
},
);
it("getSettings reads from HOMEPAGE_CONFIG_DIR and converts layout list to an object", async () => { it("getSettings reads from HOMEPAGE_CONFIG_DIR and converts layout list to an object", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "homepage-settings-test-")); const dir = mkdtempSync(path.join(tmpdir(), "homepage-settings-test-"));
process.env.HOMEPAGE_CONFIG_DIR = dir; process.env.HOMEPAGE_CONFIG_DIR = dir;
+2 -3
View File
@@ -55,7 +55,6 @@ export default async function credentialedProxyHandler(req, res, map) {
"authentik", "authentik",
"cloudflared", "cloudflared",
"ghostfolio", "ghostfolio",
"gitea",
"headscale", "headscale",
"hoarder", "hoarder",
"karakeep", "karakeep",
@@ -144,7 +143,7 @@ export default async function credentialedProxyHandler(req, res, map) {
headers["X-API-Key"] = `${widget.key}`; headers["X-API-Key"] = `${widget.key}`;
} }
const [status, contentType, data, responseHeaders] = await httpProxy(url, { const [status, contentType, data] = await httpProxy(url, {
method: req.method, method: req.method,
withCredentials: true, withCredentials: true,
credentials: "include", credentials: "include",
@@ -179,7 +178,7 @@ export default async function credentialedProxyHandler(req, res, map) {
.status(500) .status(500)
.json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } }); .json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
} }
if (map) resultData = map(resultData, responseHeaders); if (map) resultData = map(resultData);
} }
if (contentType) res.setHeader("Content-Type", contentType); if (contentType) res.setHeader("Content-Type", contentType);
@@ -45,7 +45,6 @@ vi.mock("widgets/widgets", () => ({
autobrr: { api: "{url}/{endpoint}" }, autobrr: { api: "{url}/{endpoint}" },
jellystat: { api: "{url}/{endpoint}" }, jellystat: { api: "{url}/{endpoint}" },
trilium: { api: "{url}/{endpoint}" }, trilium: { api: "{url}/{endpoint}" },
gitea: { api: "{url}/api/v1/{endpoint}" },
gitlab: { api: "{url}/{endpoint}" }, gitlab: { api: "{url}/{endpoint}" },
azuredevops: { api: "{url}/{endpoint}" }, azuredevops: { api: "{url}/{endpoint}" },
glances: { api: "{url}/{endpoint}" }, glances: { api: "{url}/{endpoint}" },
@@ -136,33 +135,6 @@ describe("utils/proxy/handlers/credentialed", () => {
expect(res.body).toEqual({ ok: true }); expect(res.body).toEqual({ ok: true });
}); });
it("uses Bearer auth for gitea widgets and keeps the key out of the URL", async () => {
getServiceWidget.mockResolvedValue({ type: "gitea", url: "http://example", key: "token" });
httpProxy.mockResolvedValue([200, "application/json", []]);
const req = { method: "GET", query: { group: "g", service: "s", endpoint: "notifications?limit=1", index: 0 } };
const res = createMockRes();
await credentialedProxyHandler(req, res);
const [url, params] = httpProxy.mock.calls[0];
expect(params.headers.Authorization).toBe("Bearer token");
expect(url.toString()).toBe("http://example/api/v1/notifications?limit=1");
});
it("passes response headers to the mapping function", async () => {
getServiceWidget.mockResolvedValue({ type: "gitea", url: "http://example", key: "token" });
httpProxy.mockResolvedValue([200, "application/json", [], { "x-total-count": "7" }]);
const req = { method: "GET", query: { group: "g", service: "s", endpoint: "notifications?limit=1", index: 0 } };
const res = createMockRes();
await credentialedProxyHandler(req, res, (data, headers) => ({ count: Number(headers["x-total-count"]) }));
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ count: 7 });
});
it("uses NC-Token auth for nextcloud widgets when key is provided", async () => { it("uses NC-Token auth for nextcloud widgets when key is provided", async () => {
getServiceWidget.mockResolvedValue({ type: "nextcloud", url: "http://example", key: "nc-token" }); getServiceWidget.mockResolvedValue({ type: "nextcloud", url: "http://example", key: "nc-token" });
httpProxy.mockResolvedValue([200, "application/json", { ok: true }]); httpProxy.mockResolvedValue([200, "application/json", { ok: true }]);
+3 -3
View File
@@ -1,4 +1,4 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; import genericProxyHandler from "utils/proxy/handlers/generic";
// Get total from header to avoid pagination limit // Get total from header to avoid pagination limit
export function totalCount(data, headers) { export function totalCount(data, headers) {
@@ -7,8 +7,8 @@ export function totalCount(data, headers) {
} }
const widget = { const widget = {
api: "{url}/api/v1/{endpoint}", api: "{url}/api/v1/{endpoint}?access_token={key}",
proxyHandler: credentialedProxyHandler, proxyHandler: genericProxyHandler,
mappings: { mappings: {
notifications: { notifications: {