From a7a4378a40d17612f3b09049d059d2caa8c6c933 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:41:55 -0700 Subject: [PATCH 1/8] Fix: ensure correct relative dates formatter uses calendar days (#7174) --- next-i18next.config.js | 6 +++ src/utils/relative-date-formatter.test.js | 46 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/utils/relative-date-formatter.test.js diff --git a/next-i18next.config.js b/next-i18next.config.js index ac1eac369..5c3613b3a 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -106,6 +106,12 @@ function relativeDate(date, formatter) { const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(delta)); const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; + if (units[unitIndex] === "day") { + // compare calendar days, not elapsed 24h blocks + const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()); + return formatter.format(Math.round((startOfDay(date) - startOfDay(new Date())) / 86400000), "day"); + } + return formatter.format(Math.floor(delta / divisor), units[unitIndex]); } diff --git a/src/utils/relative-date-formatter.test.js b/src/utils/relative-date-formatter.test.js new file mode 100644 index 000000000..baf1b0e69 --- /dev/null +++ b/src/utils/relative-date-formatter.test.js @@ -0,0 +1,46 @@ +import { createRequire } from "node:module"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const require = createRequire(import.meta.url); +const i18nextConfig = require("../../next-i18next.config.js"); + +function getRelativeDateFormatter() { + let relativeDateFormatter; + + i18nextConfig.use[0].init({ + services: { + formatter: { + add(name, formatter) { + if (name === "relativeDate") relativeDateFormatter = formatter; + }, + }, + }, + }); + + return relativeDateFormatter; +} + +describe("relativeDate formatter", () => { + const formatRelativeDate = getRelativeDateFormatter(); + const options = { numeric: "auto" }; + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2026, 8, 23, 16, 0)); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it.each([ + ["hours away on the same day", new Date(2026, 8, 23, 20, 0), "in 4 hours"], + ["next calendar day", new Date(2026, 8, 24, 20, 0), "tomorrow"], + ["two calendar days away but under 48h", new Date(2026, 8, 25, 10, 40), "in 2 days"], + ["previous calendar day", new Date(2026, 8, 22, 10, 0), "yesterday"], + ["two calendar days ago but under 48h", new Date(2026, 8, 21, 20, 0), "2 days ago"], + ])("formats %s", (_description, value, expected) => { + expect(formatRelativeDate(value.toISOString(), "en", options)).toBe(expected); + }); +}); From db480ca991fe9cc2b31ea20862ca737adb6760a2 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:32:30 -0700 Subject: [PATCH 2/8] Performance: reduce json data transformations (#7175) --- src/widgets/gitea/widget.js | 11 +++++++---- src/widgets/gitea/widget.test.js | 11 ++++++++++- src/widgets/miniflux/widget.js | 11 +++++++---- src/widgets/miniflux/widget.test.js | 10 +++++++++- src/widgets/radarr/widget.js | 23 +++++++++++++---------- src/widgets/radarr/widget.test.js | 23 +++++++++++++++++++++++ src/widgets/unmanic/widget.js | 11 +++++++---- src/widgets/unmanic/widget.test.js | 10 +++++++++- 8 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/widgets/gitea/widget.js b/src/widgets/gitea/widget.js index b0420ccc6..003a6710d 100644 --- a/src/widgets/gitea/widget.js +++ b/src/widgets/gitea/widget.js @@ -11,10 +11,13 @@ const widget = { }, issues: { endpoint: "repos/issues/search", - map: (data) => ({ - pulls: asJson(data).filter((issue) => issue.pull_request), - issues: asJson(data).filter((issue) => !issue.pull_request), - }), + map: (data) => { + const items = asJson(data); + return { + pulls: items.filter((issue) => issue.pull_request), + issues: items.filter((issue) => !issue.pull_request), + }; + }, }, repositories: { endpoint: "repos/search", diff --git a/src/widgets/gitea/widget.test.js b/src/widgets/gitea/widget.test.js index 75395c126..1039ad7ce 100644 --- a/src/widgets/gitea/widget.test.js +++ b/src/widgets/gitea/widget.test.js @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { expectWidgetConfigShape } from "test-utils/widget-config"; @@ -8,4 +8,13 @@ describe("gitea widget config", () => { it("exports a valid widget config", () => { expectWidgetConfigShape(widget); }); + + it("splits issues and pull requests", () => { + const result = widget.mappings.issues.map( + Buffer.from(JSON.stringify([{ id: 1, pull_request: {} }, { id: 2 }, { id: 3 }])), + ); + + expect(result.pulls.map((i) => i.id)).toEqual([1]); + expect(result.issues.map((i) => i.id)).toEqual([2, 3]); + }); }); diff --git a/src/widgets/miniflux/widget.js b/src/widgets/miniflux/widget.js index dbf4bbf37..aebe2cf97 100644 --- a/src/widgets/miniflux/widget.js +++ b/src/widgets/miniflux/widget.js @@ -8,10 +8,13 @@ const widget = { mappings: { counters: { endpoint: "feeds/counters", - map: (data) => ({ - read: Object.values(asJson(data).reads).reduce((acc, i) => acc + i, 0), - unread: Object.values(asJson(data).unreads).reduce((acc, i) => acc + i, 0), - }), + map: (data) => { + const { reads, unreads } = asJson(data); + return { + read: Object.values(reads).reduce((acc, i) => acc + i, 0), + unread: Object.values(unreads).reduce((acc, i) => acc + i, 0), + }; + }, }, }, }; diff --git a/src/widgets/miniflux/widget.test.js b/src/widgets/miniflux/widget.test.js index 62f7ad9b6..272d1fc64 100644 --- a/src/widgets/miniflux/widget.test.js +++ b/src/widgets/miniflux/widget.test.js @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { expectWidgetConfigShape } from "test-utils/widget-config"; @@ -8,4 +8,12 @@ describe("miniflux widget config", () => { it("exports a valid widget config", () => { expectWidgetConfigShape(widget); }); + + it("sums read and unread counters", () => { + const result = widget.mappings.counters.map( + Buffer.from(JSON.stringify({ reads: { 1: 2, 2: 3 }, unreads: { 1: 4, 2: 1 } })), + ); + + expect(result).toEqual({ read: 5, unread: 5 }); + }); }); diff --git a/src/widgets/radarr/widget.js b/src/widgets/radarr/widget.js index 5170325d6..1f307430d 100644 --- a/src/widgets/radarr/widget.js +++ b/src/widgets/radarr/widget.js @@ -1,4 +1,4 @@ -import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers"; +import { asJson } from "utils/proxy/api-helpers"; import genericProxyHandler from "utils/proxy/handlers/generic"; const widget = { @@ -8,15 +8,18 @@ const widget = { mappings: { movie: { endpoint: "movie", - map: (data) => ({ - wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length, - have: jsonArrayFilter(data, (item) => item.hasFile).length, - missing: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile).length, - all: asJson(data).map((entry) => ({ - title: entry.title, - id: entry.id, - })), - }), + map: (data) => { + const movieData = asJson(data) ?? []; + return { + wanted: movieData.filter((item) => item.monitored && !item.hasFile && item.isAvailable).length, + have: movieData.filter((item) => item.hasFile).length, + missing: movieData.filter((item) => item.monitored && !item.hasFile).length, + all: movieData.map((entry) => ({ + title: entry.title, + id: entry.id, + })), + }; + }, }, "queue/status": { endpoint: "queue/status", diff --git a/src/widgets/radarr/widget.test.js b/src/widgets/radarr/widget.test.js index c7a19f39b..9e19aecfa 100644 --- a/src/widgets/radarr/widget.test.js +++ b/src/widgets/radarr/widget.test.js @@ -33,4 +33,27 @@ describe("radarr widget config", () => { expect(queue.map((entry) => entry.movieId)).toEqual([2, 1]); }); + + it("maps movie counts and titles", () => { + const movies = widget.mappings.movie.map( + Buffer.from( + JSON.stringify([ + { id: 1, title: "A", monitored: true, hasFile: false, isAvailable: true }, + { id: 2, title: "B", monitored: true, hasFile: true }, + { id: 3, title: "C", monitored: true, hasFile: false, isAvailable: false }, + ]), + ), + ); + + expect(movies).toEqual({ + wanted: 1, + have: 1, + missing: 2, + all: [ + { title: "A", id: 1 }, + { title: "B", id: 2 }, + { title: "C", id: 3 }, + ], + }); + }); }); diff --git a/src/widgets/unmanic/widget.js b/src/widgets/unmanic/widget.js index ef4493e9a..0544c0c68 100644 --- a/src/widgets/unmanic/widget.js +++ b/src/widgets/unmanic/widget.js @@ -8,10 +8,13 @@ const widget = { mappings: { workers: { endpoint: "workers/status", - map: (data) => ({ - total_workers: asJson(data).workers_status.length, - active_workers: asJson(data).workers_status.filter((worker) => !worker.idle).length, - }), + map: (data) => { + const workers = asJson(data).workers_status; + return { + total_workers: workers.length, + active_workers: workers.filter((worker) => !worker.idle).length, + }; + }, }, pending: { method: "POST", diff --git a/src/widgets/unmanic/widget.test.js b/src/widgets/unmanic/widget.test.js index 2997a66b6..f153a6032 100644 --- a/src/widgets/unmanic/widget.test.js +++ b/src/widgets/unmanic/widget.test.js @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { expectWidgetConfigShape } from "test-utils/widget-config"; @@ -8,4 +8,12 @@ describe("unmanic widget config", () => { it("exports a valid widget config", () => { expectWidgetConfigShape(widget); }); + + it("counts total and active workers", () => { + const result = widget.mappings.workers.map( + Buffer.from(JSON.stringify({ workers_status: [{ idle: true }, { idle: false }, { idle: false }] })), + ); + + expect(result).toEqual({ total_workers: 3, active_workers: 2 }); + }); }); From 40082f798d3987da3a7241825df0be626f12e5d3 Mon Sep 17 00:00:00 2001 From: Andy George Date: Wed, 23 Sep 2026 17:39:41 -0500 Subject: [PATCH 3/8] Documentation: Arcane widget documentation formatting (#7171) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/arcane.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/widgets/services/arcane.md b/docs/widgets/services/arcane.md index 6c59b9ee3..0e1acb9a9 100644 --- a/docs/widgets/services/arcane.md +++ b/docs/widgets/services/arcane.md @@ -6,6 +6,7 @@ description: Arcane Widget Configuration Learn more about [Arcane](https://github.com/getarcaneapp/arcane). **Allowed fields** (max 4): `running`, `stopped`, `total`, `images`, `images_used`, `images_unused`, `image_updates`. + **Default fields**: `running`, `stopped`, `total`, `image_updates`. ```yaml @@ -17,4 +18,4 @@ widget: fields: ["running", "stopped", "total", "image_updates"] # optional ``` -Using an api key for the widget requires permissions for: `containers:list` `images:list and `image-updates:read` +Using an API key for the widget requires permissions for: `containers:list`, `images:list`, and `image-updates:read`. From 2e287402c4f9f104990fdfa63145fb99b3ed74ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:51:54 -0700 Subject: [PATCH 4/8] Chore(deps): Bump docker/setup-qemu-action from 4.3.0 to 4.4.0 (#7181) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6857baed7..9d7ef952d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -99,7 +99,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup QEMU - uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - name: Setup Docker buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 From c5a218c39a8cc21503ca81a46f44d27d41c5a99c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:58:27 -0700 Subject: [PATCH 5/8] Chore(deps): Bump docker/setup-buildx-action from 4.3.0 to 4.4.1 (#7185) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 9d7ef952d..27d7cf53c 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -102,7 +102,7 @@ jobs: uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - name: Setup Docker buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4 - name: Build and push Docker image id: build-and-push From b1935ed8a005b73f3dd0035d203b29a3a8848fbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:04:48 +0000 Subject: [PATCH 6/8] Chore(deps): Bump docker/build-push-action from 7.3.0 to 7.4.0 (#7182) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 27d7cf53c..1c145794f 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -106,7 +106,7 @@ jobs: - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: context: . push: ${{ github.event_name != 'pull_request' }} From 9669a6899596e8cb77066fd728de162224c46dae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:11:27 +0000 Subject: [PATCH 7/8] Chore(deps): Bump codecov/codecov-action from 7.0.0 to 7.1.1 (#7184) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index abf053948..bd3b69fdc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: # Run Vitest directly so `--shard` is parsed as an option - run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage/lcov.info From 83d7fe2556d316e8deec74aaf1d1fb0351c5d7cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:17:23 +0000 Subject: [PATCH 8/8] Chore(deps): Bump crowdin/github-action from 3.0.2 to 3.1.0 (#7183) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/crowdin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 44e859cb8..82eca75ef 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: crowdin action - uses: crowdin/github-action@0d5670f539973aea2f01abce61a8989934df0025 # v3.0.2 + uses: crowdin/github-action@9af557de76d70c480f88065d336f445a362f402b # v3.1.0 with: upload_translations: false download_translations: true