Compare commits

...

19 Commits
v1.13.0 ... dev

Author SHA1 Message Date
shamoon
9cef37aed6 Fixhancement: support dispatcharr v24 API changes (#6690)
Some checks are pending
Docker CI / Docker Build & Push (push) Waiting to run
Lint / Linting Checks (push) Waiting to run
Release Drafter / Update Release Draft (push) Waiting to run
Release Drafter / Auto Label PR (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-05-17 19:23:52 -07:00
shamoon
9a4dea39b0 Update pr-triage.yml
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Repository Maintenance / Stale (push) Has been cancelled
Repository Maintenance / Lock Old Threads (push) Has been cancelled
Repository Maintenance / Close Answered Discussions (push) Has been cancelled
Repository Maintenance / Close Outdated Discussions (push) Has been cancelled
Repository Maintenance / Close Unsupported Feature Requests (push) Has been cancelled
2026-05-15 20:23:42 -07:00
shamoon
e7a147899c PR triage workflow 2026-05-15 20:21:22 -07:00
dependabot[bot]
e81aadb071 Chore(deps): Bump pnpm/action-setup from 6.0.4 to 6.0.5 (#6677)
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Repository Maintenance / Stale (push) Has been cancelled
Repository Maintenance / Lock Old Threads (push) Has been cancelled
Repository Maintenance / Close Answered Discussions (push) Has been cancelled
Repository Maintenance / Close Outdated Discussions (push) Has been cancelled
Repository Maintenance / Close Unsupported Feature Requests (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 16:39:46 +00:00
dependabot[bot]
1e6be5cc17 Chore(deps): Bump systeminformation from 5.30.8 to 5.31.6 in the npm_and_yarn group across 1 directory (#6673)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 14:49:09 -07:00
shamoon
9b1b14116b Fix: allow explicit cookie header overwrite (#6672) 2026-05-13 08:37:33 -07:00
dependabot[bot]
02a9d74c95 Chore(deps): Bump protobufjs from 7.5.5 to 7.5.8 in the npm_and_yarn group across 1 directory (#6668)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 19:07:27 -07:00
dependabot[bot]
e7a749782a Chore(deps): Bump @protobufjs/utf8 from 1.1.0 to 1.1.1 in the npm_and_yarn group across 1 directory (#6666)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 16:02:46 -07:00
christopherjnash
f8e2e8d410 Fix: prevent Seerr widget incorrectly injecting "available" field (#6663)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-05-12 08:45:11 -07:00
dependabot[bot]
6053ddbab6 Chore(deps): Bump next from 16.2.4 to 16.2.6 in the npm_and_yarn group across 1 directory (#6657)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 10:34:27 -07:00
shamoon
11e9f28e5e Add discussion triage workflow 2026-05-11 08:50:06 -07:00
shamoon
233721cc90 1.13.1
Some checks failed
Docs / Test Build Docs (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Docs / Build & Deploy Docs (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
2026-05-11 08:42:02 -07:00
shamoon
d294a25145 Merge branch 'dev' 2026-05-11 08:13:31 -07:00
github-actions[bot]
0ff9af4c3c New Crowdin translations by GitHub Action (#6647)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-11 08:11:24 -07:00
shamoon
a6c753c8aa Fix: include tasks params for PBS widget (#6655) 2026-05-11 00:04:33 -07:00
shamoon
5d71c3aa65 Fix: allow empty data for ntfy widget (#6653) 2026-05-10 07:28:31 -07:00
shamoon
1cc4608d72 Enhancement: support qBittorrent v5.2.0 api changes (#6652) 2026-05-10 06:51:22 -07:00
shamoon
d65e5447e4 Add Issue Triage workflow 2026-05-08 16:21:48 -07:00
shamoon
d3256596d8 Enhance release-drafter autolabeler rules 2026-05-08 16:19:33 -07:00
29 changed files with 456 additions and 228 deletions

View File

@@ -47,15 +47,34 @@ categories:
- 'documentation'
autolabeler:
- label: 'bug'
title:
- '/^fix(\(.+\))?:/i'
body:
- '/- \[[xX]\] Bug fix \(non-breaking change which fixes an issue\)/'
- label: 'enhancement'
title:
- '/^(feature|enhancement)(\(.+\))?:/i'
body:
- '/- \[[xX]\] New service widget/'
- '/- \[[xX]\] New feature or enhancement \(non-breaking change which adds functionality\)/'
- label: 'documentation'
files:
- 'docs/**'
- '*.md'
- '.github/**/*.md'
title:
- '/^(documentation|docs)(\(.+\))?:/i'
body:
- '/- \[[xX]\] Documentation only/'
- label: 'chore'
body:
- '/- \[[xX]\] Other \(please explain\)/'
- label: 'ci'
files:
- '.github/workflows/**'
branch:
- '/^(ci|workflow|actions)\//'
title:
- '/^(ci|workflow|actions)(\(.+\))?:/i'
- label: 'dependencies'
files:
@@ -64,19 +83,6 @@ autolabeler:
- 'pyproject.toml'
- 'uv.lock'
- label: 'feature'
files:
- 'src/components/**'
- 'src/widgets/**'
- 'src/pages/**'
- 'src/utils/**'
- label: 'chore'
files:
- 'Dockerfile*'
- 'docker-entrypoint.sh'
- 'k3d/**'
- label: 'translation'
files:
- 'public/locales/**'

View File

@@ -61,7 +61,7 @@ jobs:
nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5
with:
version: 10
run_install: false

90
.github/workflows/issue-triage.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: Issue Triage
on:
issues:
types: [labeled]
discussion:
types: [labeled]
permissions:
discussions: write
issues: write
jobs:
close-needs-discussion:
name: Issues Need Discussion
if: github.event_name == 'issues' && github.event.label.name == 'needs-discussion'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const issueNumber = context.payload.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: 'This issue is being closed because it was opened before a maintainer asked for an issue to be created. Please start with a discussion and follow the issue template; only open an issue when a maintainer asks you to do so.',
});
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: 'closed',
state_reason: 'not_planned',
});
await github.rest.issues.lock({
owner,
repo,
issue_number: issueNumber,
lock_reason: 'off-topic',
});
comment-needs-information:
name: Discussions Need Information
if: github.event_name == 'discussion' && github.event.label.name == 'needs-information'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const discussionNumber = context.payload.discussion.number;
let discussionId = context.payload.discussion.node_id;
if (!discussionId) {
const result = await github.graphql(
`query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
discussion(number:$number) {
id
}
}
}`,
{
owner,
repo,
number: discussionNumber,
},
);
discussionId = result.repository.discussion.id;
}
await github.graphql(
`mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
clientMutationId
}
}`,
{
discussion: discussionId,
body: 'Dear homepage user, thanks for opening this discussion! Please ensure you add the output from the troubleshooting guide steps, the support template asks for those details because they usually make it possible to understand the problem without guessing. Please update the discussion with the relevant troubleshooting output, configuration, logs, and browser console details where applicable: https://gethomepage.dev/troubleshooting/. Thank you!',
},
);

View File

@@ -23,7 +23,7 @@ jobs:
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5
with:
version: 10
run_install: false

36
.github/workflows/pr-triage.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: PR Triage
on:
pull_request_target:
types: [labeled]
permissions:
issues: write
pull-requests: write
jobs:
close-needs-requirements:
name: PRs Need Requirements
if: github.event.label.name == 'needs-requirements'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pullNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
await github.rest.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body: 'Dear contributor, thanks for taking the time to open this pull request! It is being closed because it does not currently meet the pull request guidelines. Please review the checklist in the pull request template and the relevant [contributing guidelines](https://gethomepage.dev/widgets/authoring/getting-started/#new-feature-guidelines) before opening a new pull request.',
});
await github.rest.pulls.update({
owner,
repo,
pull_number: pullNumber,
state: 'closed',
});

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
- uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5
with:
version: 9

View File

@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.13.0",
"version": "1.13.1",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -28,7 +28,7 @@
"luxon": "^3.6.1",
"memory-cache": "^0.2.0",
"minecraftstatuspinger": "^1.2.2",
"next": "^16.2.4",
"next": "^16.2.6",
"next-i18next": "^15.4.3",
"ping": "^0.4.4",
"pretty-bytes": "^7.1.0",
@@ -39,7 +39,7 @@
"react-icons": "^5.6.0",
"recharts": "^3.1.2",
"swr": "^2.4.1",
"systeminformation": "^5.30.8",
"systeminformation": "^5.31.6",
"tough-cookie": "^6.0.0",
"urbackup-server-api": "^0.92.2",
"winston": "^3.19.0",

166
pnpm-lock.yaml generated
View File

@@ -51,11 +51,11 @@ importers:
specifier: ^1.2.2
version: 1.2.2
next:
specifier: ^16.2.4
version: 16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
specifier: ^16.2.6
version: 16.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
next-i18next:
specifier: ^15.4.3
version: 15.4.3(@types/react@19.0.10)(i18next@25.10.9(typescript@5.7.3))(next@16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-i18next@15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.7.3))(react@19.2.5)
version: 15.4.3(@types/react@19.0.10)(i18next@25.10.9(typescript@5.7.3))(next@16.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-i18next@15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.7.3))(react@19.2.5)
ping:
specifier: ^0.4.4
version: 0.4.4
@@ -84,8 +84,8 @@ importers:
specifier: ^2.4.1
version: 2.4.1(react@19.2.5)
systeminformation:
specifier: ^5.30.8
version: 5.30.8
specifier: ^5.31.6
version: 5.31.6
tough-cookie:
specifier: ^6.0.0
version: 6.0.0
@@ -773,56 +773,56 @@ packages:
'@napi-rs/wasm-runtime@0.2.8':
resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==}
'@next/env@16.2.4':
resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==}
'@next/env@16.2.6':
resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
'@next/eslint-plugin-next@15.5.11':
resolution: {integrity: sha512-tS/HYQOjIoX9ZNDQitba/baS8sTvo3ekY6Vgdx5lmhN4jov082bdApIChXr94qhMZHvEciz9DZglFFnhguQp/A==}
'@next/swc-darwin-arm64@16.2.4':
resolution: {integrity: sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==}
'@next/swc-darwin-arm64@16.2.6':
resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@16.2.4':
resolution: {integrity: sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==}
'@next/swc-darwin-x64@16.2.6':
resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@16.2.4':
resolution: {integrity: sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==}
'@next/swc-linux-arm64-gnu@16.2.6':
resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@16.2.4':
resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==}
'@next/swc-linux-arm64-musl@16.2.6':
resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@16.2.4':
resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==}
'@next/swc-linux-x64-gnu@16.2.6':
resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@16.2.4':
resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==}
'@next/swc-linux-x64-musl@16.2.6':
resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@16.2.4':
resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==}
'@next/swc-win32-arm64-msvc@16.2.6':
resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@16.2.4':
resolution: {integrity: sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==}
'@next/swc-win32-x64-msvc@16.2.6':
resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -857,8 +857,8 @@ packages:
'@protobufjs/base64@1.1.2':
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
'@protobufjs/codegen@2.0.4':
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
'@protobufjs/codegen@2.0.5':
resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==}
'@protobufjs/eventemitter@1.1.0':
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
@@ -869,8 +869,8 @@ packages:
'@protobufjs/float@1.0.2':
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
'@protobufjs/inquire@1.1.0':
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
'@protobufjs/inquire@1.1.1':
resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==}
'@protobufjs/path@1.1.2':
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
@@ -878,8 +878,8 @@ packages:
'@protobufjs/pool@1.1.0':
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@protobufjs/utf8@1.1.1':
resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==}
'@react-aria/focus@3.22.0':
resolution: {integrity: sha512-ZfDOVuVhqDsM9mkNji3QUZ/d40JhlVgXrDkrfXylM1035QCrcTHN7m2DpbE95sU2A8EQb4wikvt5jM6K/73BPg==}
@@ -1577,8 +1577,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.10.27:
resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==}
baseline-browser-mapping@2.10.29:
resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -1644,8 +1644,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001791:
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
caniuse-lite@1.0.30001792:
resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
@@ -2888,8 +2888,8 @@ packages:
react: '>= 17.0.2'
react-i18next: '>= 13.5.0'
next@16.2.4:
resolution: {integrity: sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==}
next@16.2.6:
resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -3092,8 +3092,8 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
protobufjs@7.5.5:
resolution: {integrity: sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==}
protobufjs@7.5.8:
resolution: {integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==}
engines: {node: '>=12.0.0'}
pump@3.0.4:
@@ -3309,8 +3309,8 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
semver@7.8.0:
resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==}
engines: {node: '>=10'}
hasBin: true
@@ -3504,8 +3504,8 @@ packages:
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
engines: {node: ^14.18.0 || >=16.0.0}
systeminformation@5.30.8:
resolution: {integrity: sha512-imB8LwJCc2DkufKlSRHfzbjhheGzpg1P31A4c55IKTq/ll6Agn1rhBOY+WmS/hyg5inGFp7AyZIK0gvq5rFO2Q==}
systeminformation@5.31.6:
resolution: {integrity: sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==}
engines: {node: '>=8.0.0'}
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
@@ -4187,14 +4187,14 @@ snapshots:
dependencies:
lodash.camelcase: 4.3.0
long: 5.3.2
protobufjs: 7.5.5
protobufjs: 7.5.8
yargs: 17.7.2
'@grpc/proto-loader@0.8.0':
dependencies:
lodash.camelcase: 4.3.0
long: 5.3.2
protobufjs: 7.5.5
protobufjs: 7.5.8
yargs: 17.7.2
'@headlessui/react@2.2.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
@@ -4405,34 +4405,34 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
'@next/env@16.2.4': {}
'@next/env@16.2.6': {}
'@next/eslint-plugin-next@15.5.11':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@16.2.4':
'@next/swc-darwin-arm64@16.2.6':
optional: true
'@next/swc-darwin-x64@16.2.4':
'@next/swc-darwin-x64@16.2.6':
optional: true
'@next/swc-linux-arm64-gnu@16.2.4':
'@next/swc-linux-arm64-gnu@16.2.6':
optional: true
'@next/swc-linux-arm64-musl@16.2.4':
'@next/swc-linux-arm64-musl@16.2.6':
optional: true
'@next/swc-linux-x64-gnu@16.2.4':
'@next/swc-linux-x64-gnu@16.2.6':
optional: true
'@next/swc-linux-x64-musl@16.2.4':
'@next/swc-linux-x64-musl@16.2.6':
optional: true
'@next/swc-win32-arm64-msvc@16.2.4':
'@next/swc-win32-arm64-msvc@16.2.6':
optional: true
'@next/swc-win32-x64-msvc@16.2.4':
'@next/swc-win32-x64-msvc@16.2.6':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -4458,24 +4458,24 @@ snapshots:
'@protobufjs/base64@1.1.2': {}
'@protobufjs/codegen@2.0.4': {}
'@protobufjs/codegen@2.0.5': {}
'@protobufjs/eventemitter@1.1.0': {}
'@protobufjs/fetch@1.1.0':
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/inquire': 1.1.0
'@protobufjs/inquire': 1.1.1
'@protobufjs/float@1.0.2': {}
'@protobufjs/inquire@1.1.0': {}
'@protobufjs/inquire@1.1.1': {}
'@protobufjs/path@1.1.2': {}
'@protobufjs/pool@1.1.0': {}
'@protobufjs/utf8@1.1.0': {}
'@protobufjs/utf8@1.1.1': {}
'@react-aria/focus@3.22.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
@@ -4867,7 +4867,7 @@ snapshots:
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.7.4
semver: 7.8.0
ts-api-utils: 2.1.0(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
@@ -5154,7 +5154,7 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.10.27: {}
baseline-browser-mapping@2.10.29: {}
bcrypt-pbkdf@1.0.2:
dependencies:
@@ -5231,7 +5231,7 @@ snapshots:
callsites@3.1.0: {}
caniuse-lite@1.0.30001791: {}
caniuse-lite@1.0.30001792: {}
chai@5.3.3:
dependencies:
@@ -5457,7 +5457,7 @@ snapshots:
'@grpc/grpc-js': 1.14.3
'@grpc/proto-loader': 0.7.15
docker-modem: 5.0.7
protobufjs: 7.5.5
protobufjs: 7.5.8
tar-fs: 2.1.4
uuid: 10.0.0
transitivePeerDependencies:
@@ -6252,7 +6252,7 @@ snapshots:
is-bun-module@2.0.0:
dependencies:
semver: 7.7.4
semver: 7.8.0
is-callable@1.2.7: {}
@@ -6573,7 +6573,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
semver: 7.7.4
semver: 7.8.0
math-intrinsics@1.1.0: {}
@@ -6637,7 +6637,7 @@ snapshots:
net@1.0.2: {}
next-i18next@15.4.3(@types/react@19.0.10)(i18next@25.10.9(typescript@5.7.3))(next@16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-i18next@15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.7.3))(react@19.2.5):
next-i18next@15.4.3(@types/react@19.0.10)(i18next@25.10.9(typescript@5.7.3))(next@16.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-i18next@15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.7.3))(react@19.2.5):
dependencies:
'@babel/runtime': 7.28.6
'@types/hoist-non-react-statics': 3.3.7(@types/react@19.0.10)
@@ -6645,31 +6645,31 @@ snapshots:
hoist-non-react-statics: 3.3.2
i18next: 25.10.9(typescript@5.7.3)
i18next-fs-backend: 2.6.4
next: 16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
next: 16.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-i18next: 15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.7.3)
transitivePeerDependencies:
- '@types/react'
next@16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
next@16.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@next/env': 16.2.4
'@next/env': 16.2.6
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.27
caniuse-lite: 1.0.30001791
baseline-browser-mapping: 2.10.29
caniuse-lite: 1.0.30001792
postcss: 8.4.31
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
styled-jsx: 5.1.6(react@19.2.5)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.4
'@next/swc-darwin-x64': 16.2.4
'@next/swc-linux-arm64-gnu': 16.2.4
'@next/swc-linux-arm64-musl': 16.2.4
'@next/swc-linux-x64-gnu': 16.2.4
'@next/swc-linux-x64-musl': 16.2.4
'@next/swc-win32-arm64-msvc': 16.2.4
'@next/swc-win32-x64-msvc': 16.2.4
'@next/swc-darwin-arm64': 16.2.6
'@next/swc-darwin-x64': 16.2.6
'@next/swc-linux-arm64-gnu': 16.2.6
'@next/swc-linux-arm64-musl': 16.2.6
'@next/swc-linux-x64-gnu': 16.2.6
'@next/swc-linux-x64-musl': 16.2.6
'@next/swc-win32-arm64-msvc': 16.2.6
'@next/swc-win32-x64-msvc': 16.2.6
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -6847,18 +6847,18 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
protobufjs@7.5.5:
protobufjs@7.5.8:
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2
'@protobufjs/codegen': 2.0.4
'@protobufjs/codegen': 2.0.5
'@protobufjs/eventemitter': 1.1.0
'@protobufjs/fetch': 1.1.0
'@protobufjs/float': 1.0.2
'@protobufjs/inquire': 1.1.0
'@protobufjs/inquire': 1.1.1
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@protobufjs/utf8': 1.1.1
'@types/node': 25.5.0
long: 5.3.2
@@ -7124,7 +7124,7 @@ snapshots:
semver@6.3.1: {}
semver@7.7.4: {}
semver@7.8.0: {}
set-function-length@1.2.2:
dependencies:
@@ -7154,7 +7154,7 @@ snapshots:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.7.4
semver: 7.8.0
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
@@ -7375,7 +7375,7 @@ snapshots:
dependencies:
'@pkgr/core': 0.2.9
systeminformation@5.30.8: {}
systeminformation@5.31.6: {}
tabbable@6.4.0: {}

View File

@@ -344,15 +344,15 @@
"address": "Adresse",
"expires": "Expire",
"never": "Jamais",
"user": "User",
"hostname": "Hostname",
"name": "Name",
"client_version": "Client Version",
"user": "Utilisateur",
"hostname": "Nom d'hôte",
"name": "Nom",
"client_version": "Version client",
"os": "OS",
"created": "Created",
"authorized": "Authorized",
"is_external": "Is External",
"update_available": "Update Available",
"created": "Créé",
"authorized": "Autorisée",
"is_external": "Externe",
"update_available": "Mise à jour disponible",
"tags": "Tags",
"last_seen": "Vu pour la dernière fois",
"now": "Maintenant",
@@ -363,8 +363,8 @@
"minutes": "{{number}}m",
"seconds": "{{number}}s",
"ago": "Il y a {{value}}",
"true": "Yes",
"false": "No"
"true": "Oui",
"false": "Non"
},
"technitium": {
"totalQueries": "Requêtes",
@@ -937,16 +937,16 @@
"criticals": "Urgent"
},
"ntfy": {
"title": "Title",
"priority": "Priority",
"lastReceived": "Last Received",
"title": "Titre",
"priority": "Priorité",
"lastReceived": "Dernière réception",
"message": "Message",
"tags": "Tags",
"none": "None",
"none": "Aucun",
"min": "Min",
"low": "Low",
"default": "Default",
"high": "High",
"low": "Bas",
"default": "Défaut",
"high": "Haut",
"urgent": "Urgent"
},
"plantit": {

View File

@@ -67,14 +67,14 @@
"empty_data": "Статус подсистемы неизвестен"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "Здоров",
"degraded": "Деградация",
"no_data": "Нет доступных данных о хранилище"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "Память",
"mem": "ОЗУ",
"cpu": "ЦП",
"running": "Запущено",
"offline": "Не в сети",
@@ -184,15 +184,15 @@
},
"tautulli": {
"playing": "Играет",
"transcoding": "Транскодируется",
"transcoding": "Перекодируется",
"bitrate": "Битрейт",
"no_active": "Нет активных стримов",
"no_active": "Нет активных потоков",
"plex_connection_error": "Проверка соединения Plex"
},
"tracearr": {
"no_active": "Нет активных потоков",
"streams": "Потоки",
"transcodes": "Transcodes",
"transcodes": "Перекодирования",
"directplay": "Прямое воспроизведение",
"bitrate": "Битрейт"
},
@@ -215,18 +215,18 @@
"tv": "Сериалы"
},
"sabnzbd": {
"rate": "",
"rate": "Скорость",
"queue": "Очередь",
"timeleft": "Осталось"
},
"rutorrent": {
"active": "Активно",
"upload": "Загрузка",
"upload": "Отдача",
"download": "Скачивание"
},
"transmission": {
"download": "Скачивание",
"upload": "Загрузка",
"upload": "Отдача",
"leech": "Лич",
"seed": "Сид"
},
@@ -295,7 +295,7 @@
"available": "Доступно"
},
"seerr": {
"pending": "Pending",
"pending": "Ожидают",
"approved": "Одобрено",
"available": "Доступно",
"completed": "Завершено",
@@ -344,16 +344,16 @@
"address": "Адрес",
"expires": "Истекает",
"never": "Никогда",
"user": "User",
"hostname": "Hostname",
"name": "Name",
"client_version": "Client Version",
"os": "OS",
"created": "Created",
"authorized": "Authorized",
"is_external": "Is External",
"update_available": "Update Available",
"tags": "Tags",
"user": "Пользователь",
"hostname": "Имя хоста",
"name": "Имя",
"client_version": "Версия клиента",
"os": "ОС",
"created": "Создано",
"authorized": "Авторизовано",
"is_external": "Внешний",
"update_available": "Доступно обновление",
"tags": "Теги",
"last_seen": "Последнее посещение",
"now": "Только что",
"years": "{{number}}г",
@@ -363,8 +363,8 @@
"minutes": "{{number}}м",
"seconds": "{{number}}с",
"ago": "{{value}} назад",
"true": "Yes",
"false": "No"
"true": "Да",
"false": "Нет"
},
"technitium": {
"totalQueries": "Запросы",
@@ -632,12 +632,12 @@
},
"pangolin": {
"orgs": "Orgs",
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
"out": "Out"
"sites": "Сайты",
"resources": "Ресурсы",
"targets": "Цели",
"traffic": "Трафик",
"in": "Входящий",
"out": "Исходящий"
},
"peanut": {
"battery_charge": "Заряд батареи",
@@ -736,8 +736,8 @@
"volumeAvailable": "Доступно"
},
"dispatcharr": {
"channels": "Channels",
"streams": "Streams"
"channels": "Каналы",
"streams": "Потоки"
},
"mylar": {
"series": "Серии",
@@ -828,10 +828,10 @@
"series": "Серии"
},
"booklore": {
"libraries": "Libraries",
"books": "Books",
"reading": "Reading",
"finished": "Finished"
"libraries": "Библиотеки",
"books": "Книги",
"reading": "Читаю",
"finished": "Завершено"
},
"jdownloader": {
"downloadCount": "Очередь",
@@ -937,17 +937,17 @@
"criticals": "Критические"
},
"ntfy": {
"title": "Title",
"priority": "Priority",
"title": "Название",
"priority": "Приоритет",
"lastReceived": "Last Received",
"message": "Message",
"tags": "Tags",
"none": "None",
"min": "Min",
"low": "Low",
"default": "Default",
"high": "High",
"urgent": "Urgent"
"message": "Сообщение",
"tags": "Теги",
"none": "Отсутствует",
"min": "Минимальный",
"low": "Низкий",
"default": "По-умолчанию",
"high": "Высокий",
"urgent": "Срочный"
},
"plantit": {
"events": "События",
@@ -1087,7 +1087,7 @@
},
"gitlab": {
"groups": "Группы",
"issues": "Issues",
"issues": "Задачи",
"merges": "Мердж-реквесты",
"projects": "Проекты"
},
@@ -1150,26 +1150,26 @@
"nextRenewingSubscription": "Следующая оплата"
},
"unraid": {
"STARTED": "Started",
"STOPPED": "Stopped",
"STARTED": "Запущено",
"STOPPED": "Остановлено",
"NEW_ARRAY": "Новый массив",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",
"SWAP_DSBL": "Swap Disable",
"INVALID_EXPANSION": "Invalid Expansion",
"RECON_DISK": "Восстановление Диска",
"DISABLE_DISK": "Диск отключен",
"SWAP_DSBL": "Swap отключён",
"INVALID_EXPANSION": "Неверное Расширение",
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
"NO_DATA_DISKS": "No Data Disks",
"TOO_MANY_MISSING_DISKS": "Слишком много отсутствующих дисков",
"NEW_DISK_TOO_SMALL": "Новый диск слишком мал",
"NO_DATA_DISKS": "Нет дисков данных",
"notifications": "Уведомления",
"status": "Статус",
"cpu": "ЦП",
"memoryUsed": "Использовано ОЗУ",
"memoryAvailable": "Memory Available",
"memoryAvailable": "Доступная память",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"poolUsed": "{{pool}} Used",
"poolFree": "{{pool}} Free"
"poolUsed": "{{pool}} Использовано",
"poolFree": "{{pool}} Свободно"
},
"backrest": {
"num_plans": "Plans",
@@ -1180,29 +1180,29 @@
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"songs": "Треков",
"time": "Время",
"artists": "Artists"
"artists": "Исполнителей"
},
"arcane": {
"containers": "Containers",
"images": "Images",
"image_updates": "Image Updates",
"images_unused": "Unused",
"environment_required": "Environment ID Required"
"containers": "Контейнеров",
"images": "Образов",
"image_updates": "Обновлений",
"images_unused": "Не используется",
"environment_required": "Требуется ID окружения"
},
"dockhand": {
"running": "Running",
"stopped": "Stopped",
"cpu": "CPU",
"memory": "Memory",
"images": "Images",
"volumes": "Volumes",
"events_today": "Events Today",
"pending_updates": "Pending Updates",
"stacks": "Stacks",
"paused": "Paused",
"total": "Total",
"running": "Запущено",
"stopped": "Остановлено",
"cpu": "ЦП",
"memory": "ОЗУ",
"images": "Образов",
"volumes": "Томов",
"events_today": "Событий сегодня",
"pending_updates": "Обновлений",
"stacks": "Стеков",
"paused": "На паузе",
"total": "Всего",
"environment_not_found": "Среда не найдена"
},
"sparkyfitness": {

View File

@@ -2,13 +2,13 @@ import { Cookie, CookieJar } from "tough-cookie";
const cookieJar = new CookieJar();
export function setCookieHeader(url, params) {
export function setCookieHeader(url, params, { overwrite = false } = {}) {
// add cookie header, if we have one in the jar
const existingCookie = cookieJar.getCookieStringSync(url.toString());
if (existingCookie) {
params.headers = params.headers ?? {};
const cookieHeader = params.cookieHeader ?? "Cookie";
if (!params.headers[cookieHeader]) {
if (overwrite || !params.headers[cookieHeader]) {
params.headers[cookieHeader] = existingCookie;
}
}

View File

@@ -54,4 +54,17 @@ describe("utils/proxy/cookie-jar", () => {
expect(params.headers.Cookie).toBe("manual=1");
});
it("overwrites an existing cookie header when requested", async () => {
const { addCookieToJar, setCookieHeader } = await import("./cookie-jar");
const url = new URL("http://example5.test/path");
addCookieToJar(url, { "set-cookie": ["sid=1; Path=/"] });
const params = { headers: { Cookie: "stale=1" } };
setCookieHeader(url, params, { overwrite: true });
expect(params.headers.Cookie).toContain("sid=1");
expect(params.headers.Cookie).not.toContain("stale=1");
});
});

View File

@@ -97,7 +97,7 @@ export default function createUnifiProxyHandler({
}
addCookieToJar(url, responseHeaders);
setCookieHeader(url, params);
setCookieHeader(url, params, { overwrite: true });
[status, contentType, data, responseHeaders] = await httpProxy(url, params);
}

View File

@@ -18,7 +18,7 @@ function addCookieHandler(url, params) {
// handle cookies during redirects
params.beforeRedirect = (options, responseInfo) => {
addCookieToJar(options.href, responseInfo.headers);
setCookieHeader(options.href, options);
setCookieHeader(options.href, options, { overwrite: true });
};
}

View File

@@ -348,7 +348,9 @@ describe("utils/proxy/http httpProxy", () => {
);
expect(cookieJar.addCookieToJar).toHaveBeenCalledWith("http://example.com/redirect", { "set-cookie": ["a=b"] });
expect(cookieJar.setCookieHeader).toHaveBeenCalledWith("http://example.com/redirect", expect.any(Object));
expect(cookieJar.setCookieHeader).toHaveBeenCalledWith("http://example.com/redirect", expect.any(Object), {
overwrite: true,
});
});
it("supports gzip-compressed responses", async () => {

View File

@@ -8,6 +8,13 @@ export default function validateWidgetData(widget, endpoint, data) {
let dataParsed = data;
let error;
let mapping;
const mappings = widgets[widget.type]?.mappings;
if (mappings) {
mapping = Object.values(mappings).find((m) => m.endpoint === endpoint);
}
if (mapping?.allowEmpty && Buffer.isBuffer(data) && data.length === 0) return true;
if (Buffer.isBuffer(data)) {
try {
dataParsed = JSON.parse(data);
@@ -23,15 +30,11 @@ export default function validateWidgetData(widget, endpoint, data) {
}
if (dataParsed && Object.entries(dataParsed).length) {
const mappings = widgets[widget.type]?.mappings;
if (mappings) {
mapping = Object.values(mappings).find((m) => m.endpoint === endpoint);
mapping?.validate?.forEach((key) => {
if (dataParsed[key] === undefined) {
valid = false;
}
});
}
mapping?.validate?.forEach((key) => {
if (dataParsed[key] === undefined) {
valid = false;
}
});
}
if (!valid) {

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { loggerError } = vi.hoisted(() => ({
loggerError: vi.fn(),
@@ -18,6 +18,10 @@ vi.mock("widgets/widgets", () => ({
endpoint: "foo",
validate: ["a", "b"],
},
empty: {
endpoint: "empty",
allowEmpty: true,
},
},
},
},
@@ -26,6 +30,10 @@ vi.mock("widgets/widgets", () => ({
import validateWidgetData from "./validate-widget-data";
describe("utils/proxy/validate-widget-data", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns false when buffer JSON cannot be parsed", () => {
expect(validateWidgetData({ type: "test" }, "foo", Buffer.from("not json"))).toBe(false);
expect(loggerError).toHaveBeenCalled();
@@ -41,4 +49,9 @@ describe("utils/proxy/validate-widget-data", () => {
expect(validateWidgetData({ type: "test" }, "foo", Buffer.from(JSON.stringify({ a: 1 })))).toBe(false);
expect(loggerError).toHaveBeenCalled();
});
it("allows empty buffer responses for mappings that explicitly allow them", () => {
expect(validateWidgetData({ type: "test" }, "empty", Buffer.from(""))).toBe(true);
expect(loggerError).not.toHaveBeenCalled();
});
});

View File

@@ -50,10 +50,10 @@ export default function Component({ service }) {
streams?.channels &&
streams.channels.map((activeStream) => (
<StreamEntry
title={activeStream.stream_name}
title={activeStream.channel_name ?? activeStream.stream_name}
clients={activeStream.clients.length}
bitrate={activeStream.avg_bitrate}
key={activeStream.stream_name}
key={activeStream.channel_name ?? activeStream.stream_name}
/>
))}
</>

View File

@@ -32,7 +32,7 @@ describe("widgets/dispatcharr/component", () => {
useWidgetAPI.mockReturnValueOnce({ data: [{}, {}, {}], error: undefined }).mockReturnValueOnce({
data: {
count: 1,
channels: [{ stream_name: "Stream1", clients: [{}, {}], avg_bitrate: "1000kbps" }],
channels: [{ channel_name: "Stream1", clients: [{}, {}], avg_bitrate: "1000kbps" }],
},
error: undefined,
});

View File

@@ -1,7 +1,7 @@
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import { asJson, formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
import { addCookieToJar } from "utils/proxy/cookie-jar";
import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
@@ -57,6 +57,7 @@ export default async function frigateProxyHandler(req, res, map) {
}
addCookieToJar(url, loginResponseHeaders);
setCookieHeader(url, params, { overwrite: true });
// Retry original request with cookie set
[status, , data] = await httpProxy(url, params);
}

View File

@@ -7,6 +7,7 @@ const { httpProxy, getServiceWidget, cookieJar, logger } = vi.hoisted(() => ({
getServiceWidget: vi.fn(),
cookieJar: {
addCookieToJar: vi.fn(),
setCookieHeader: vi.fn(),
},
logger: {
debug: vi.fn(),
@@ -130,6 +131,9 @@ describe("widgets/frigate/proxy", () => {
await frigateProxyHandler(req, res);
expect(cookieJar.addCookieToJar).toHaveBeenCalled();
expect(cookieJar.setCookieHeader).toHaveBeenCalledWith("http://frigate/api/stats", expect.any(Object), {
overwrite: true,
});
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ num_cameras: 2, uptime: 123, version: "1.0" });
});

View File

@@ -1,5 +1,14 @@
import { asJson } from "utils/proxy/api-helpers";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const noMessages = {
title: null,
message: null,
priority: 3,
time: null,
tags: [],
};
const widget = {
api: "{url}/{endpoint}",
proxyHandler: credentialedProxyHandler,
@@ -7,6 +16,14 @@ const widget = {
mappings: {
messages: {
endpoint: "{topic}/json?poll=1&since=latest",
allowEmpty: true,
map: (data) => {
if (Buffer.isBuffer(data) && data.length === 0) {
return noMessages;
}
return asJson(data);
},
},
},
};

View File

@@ -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,18 @@ describe("ntfy widget config", () => {
it("exports a valid widget config", () => {
expectWidgetConfigShape(widget);
});
it("maps an empty latest message response to the no messages state", () => {
expect(widget.mappings.messages.map(Buffer.from(""))).toEqual({
title: null,
message: null,
priority: 3,
time: null,
tags: [],
});
});
it("parses latest message responses", () => {
expect(widget.mappings.messages.map(Buffer.from('{"message":"hello"}'))).toEqual({ message: "hello" });
});
});

View File

@@ -10,6 +10,7 @@ const widget = {
},
"nodes/localhost/tasks": {
endpoint: "nodes/localhost/tasks",
params: ["errors", "limit", "since"],
},
"nodes/localhost/status": {
endpoint: "nodes/localhost/status",

View File

@@ -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,8 @@ describe("proxmoxbackupserver widget config", () => {
it("exports a valid widget config", () => {
expectWidgetConfigShape(widget);
});
it("requires failed task query params for the tasks endpoint", () => {
expect(widget.mappings["nodes/localhost/tasks"].params).toEqual(["errors", "limit", "since"]);
});
});

View File

@@ -41,12 +41,12 @@ export default async function qbittorrentProxyHandler(req, res) {
if (status === 403) {
[status, data] = await login(widget);
if (status !== 200) {
if (![200, 204].includes(status)) {
logger.error("HTTP %d logging in to qBittorrent. Data: %s", status, data);
return res.status(status).end(data);
}
if (data.toString() !== "Ok.") {
if (status === 200 && data.toString() !== "Ok.") {
logger.error("Error logging in to qBittorrent: Data: %s", data);
return res.status(401).end(data);
}

View File

@@ -49,6 +49,25 @@ describe("widgets/qbittorrent/proxy", () => {
expect(res.body).toEqual(Buffer.from("data"));
});
it("accepts qBittorrent 5.2.0 no-content login responses", async () => {
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });
httpProxy
.mockResolvedValueOnce([403, "application/json", Buffer.from("nope")])
.mockResolvedValueOnce([204, null, Buffer.from("")])
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
const req = { query: { group: "g", service: "svc", endpoint: "torrents/info", index: "0" } };
const res = createMockRes();
await qbittorrentProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(3);
expect(httpProxy.mock.calls[1][0]).toBe("http://qb/api/v2/auth/login");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("data"));
});
it("returns 401 when login succeeds but response body is not Ok.", async () => {
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });

View File

@@ -30,10 +30,12 @@ export default function Component({ service }) {
);
}
if (statsData.completed === undefined) {
// Newer versions added "completed", fallback to available
widget.fields = widget.fields.filter((field) => field !== "completed");
widget.fields.push("available");
if (
statsData.completed === undefined &&
(widget.fields.includes("completed") || widget.fields.includes("available"))
) {
// Fallback to "available" if "completed" requested but not available
widget.fields = widget.fields.map((field) => (field === "completed" ? "available" : field));
}
return (

View File

@@ -86,6 +86,9 @@ describe("widgets/unifi/proxy", () => {
expect(httpProxy).toHaveBeenCalledTimes(4);
expect(httpProxy.mock.calls[1][0].toString()).toContain("/proxy/network/api/self");
expect(cookieJar.addCookieToJar).toHaveBeenCalled();
expect(cookieJar.setCookieHeader).toHaveBeenLastCalledWith(expect.any(URL), expect.any(Object), {
overwrite: true,
});
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("data"));
});