Compare commits

..

1 Commits

Author SHA1 Message Date
shamoon
15fa053b4d Enhancement: support completed field for jellyseer widget 2025-12-11 06:29:19 -08:00
29 changed files with 321 additions and 573 deletions

View File

@@ -84,7 +84,7 @@ jobs:
latest=auto
- name: Next.js build cache
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: .next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx') }}

View File

@@ -37,7 +37,7 @@ jobs:
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v5
- uses: actions/cache@v4
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
@@ -63,7 +63,7 @@ jobs:
with:
python-version: 3.x
- run: echo "cache_id=${{github.sha}}" >> $GITHUB_ENV
- uses: actions/cache@v5
- uses: actions/cache@v4
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache

View File

@@ -32,7 +32,7 @@ jobs:
name: 'Lock Old Threads'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v6
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: '30'
pr-inactive-days: '30'

View File

@@ -22,9 +22,7 @@ if [ "$HOSTNAME" = "::" ]; then
fi
# Check ownership before chown
if [ "$PUID" = "0" ]; then
echo "Skipping ownership changes for /app/config"
elif [ -e /app/config ]; then
if [ -e /app/config ]; then
CURRENT_UID=$(stat -c %u /app/config)
CURRENT_GID=$(stat -c %g /app/config)
@@ -41,9 +39,7 @@ else
fi
# Ensure /app/config/logs exists and is owned
if [ "$PUID" = "0" ]; then
echo "Skipping ownership changes for /app/config/logs"
elif [ -n "$PUID" ] && [ -n "$PGID" ]; then
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
mkdir -p /app/config/logs 2>/dev/null || true
if [ -d /app/config/logs ]; then
LOG_UID=$(stat -c %u /app/config/logs)

View File

@@ -189,8 +189,6 @@ labels: ...
- homepage.widgets[1].slug=youreventslughere
```
To pass custom HTTP headers with a widget request when using labels, use the same dot-notation: `homepage.widget.headers.X-Auth-Key=secret` (or `homepage.widgets[0].headers.X-Auth-Key=secret` when multiple widgets are present).
You can add specify fields for e.g. the [CustomAPI](../widgets/services/customapi.md) widget by using array-style dot notation:
```yaml

View File

@@ -94,7 +94,6 @@ metadata:
gethomepage.dev/name: Emby
gethomepage.dev/widget.type: "emby"
gethomepage.dev/widget.url: "https://emby.example.com"
gethomepage.dev/widget.headers.X-Auth-Key: "your-secret-here"
gethomepage.dev/pod-selector: ""
gethomepage.dev/weight: 10 # optional
gethomepage.dev/instance: "public" # optional

View File

@@ -101,25 +101,6 @@ Each service can have multiple widgets attached to it, for example:
Multiple widgets per service are not yet supported with Kubernetes ingress annotations.
#### Custom HTTP headers
Widgets that make HTTP calls support extra request headers via `headers`. This is useful when a reverse proxy expects a secret header.
```yaml
- UptimeRobot:
icon: uptimekuma.png
href: https://uptimerobot.com/
widget:
type: uptimerobot
url: https://api.uptimerobot.com
key: ${UPTIMEROBOT_API_KEY}
headers:
User-Agent: homepage
X-Auth-Key: your-secret-here
```
If you define services via Docker labels or Kubernetes annotations, use the same key with dot-notation (for example `homepage.widget.headers.X-Auth-Key=secret` or `gethomepage.dev/widget.headers.X-Auth-Key: "secret"`).
#### Field Visibility
Each widget can optionally provide a list of which fields should be visible via the `fields` widget property. If no fields are specified, then all fields will be displayed. The `fields` property must be a valid YAML array of strings. As an example, here is the entry for Sonarr showing only a couple of fields.

View File

@@ -7,8 +7,8 @@ Learn more about [Jellyseerr](https://github.com/Fallenbagel/jellyseerr).
Find your API key under `Settings > General > API Key`.
Allowed fields: `["pending", "approved", "available", "issues"]`.
Default fields: `["pending", "approved", "available"]`.
Allowed fields: `["pending", "approved", "available", "completed", "issues"]`.
Default fields: `["pending", "approved", "completed"]`.
```yaml
widget:

View File

@@ -5,11 +5,6 @@ description: TrueNas Scale Widget Configuration
Learn more about [TrueNas](https://www.truenas.com/).
| TrueNAS Version | Homepage widget version |
| ----------------------- | ----------------------- |
| < 26.04 (REST API) | 1 (default) |
| > 25.04 (Websocket API) | 2 |
Allowed fields: `["load", "uptime", "alerts"]`.
To create an API Key, follow [the official TrueNAS documentation](https://www.truenas.com/docs/scale/scaletutorials/toptoolbar/managingapikeys/).
@@ -22,7 +17,6 @@ To use the `enablePools` option with TrueNAS Core, the `nasType` parameter is re
widget:
type: truenas
url: http://truenas.host.or.ip
version: 2 # optional, defaults to 1
username: user # not required if using api key
password: pass # not required if using api key
key: yourtruenasapikey # not required if using username / password

View File

@@ -20,12 +20,12 @@
"gamedig": "^5.3.2",
"i18next": "^25.5.3",
"ical.js": "^2.1.0",
"js-yaml": "^4.1.1",
"js-yaml": "^4.1.0",
"json-rpc-2.0": "^1.7.0",
"luxon": "^3.6.1",
"memory-cache": "^0.2.0",
"minecraftstatuspinger": "^1.2.2",
"next": "^15.5.9",
"next": "^15.5.7",
"next-i18next": "^12.1.0",
"ping": "^0.4.4",
"pretty-bytes": "^7.1.0",
@@ -33,19 +33,18 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.5.3",
"react-icons": "^5.5.0",
"react-icons": "^5.4.0",
"recharts": "^3.1.2",
"swr": "^2.3.3",
"systeminformation": "^5.27.11",
"tough-cookie": "^6.0.0",
"urbackup-server-api": "^0.91.0",
"urbackup-server-api": "^0.8.9",
"winston": "^3.17.0",
"ws": "^8.18.3",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/postcss": "^4.1.14",
"eslint": "^9.25.1",
"eslint-config-next": "^15.2.4",
"eslint-config-prettier": "^10.1.8",
@@ -66,7 +65,6 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"@tailwindcss/oxide",
"osx-temperature-sensor",
"sharp"
]

382
pnpm-lock.yaml generated
View File

@@ -36,8 +36,8 @@ importers:
specifier: ^2.1.0
version: 2.1.0
js-yaml:
specifier: ^4.1.1
version: 4.1.1
specifier: ^4.1.0
version: 4.1.0
json-rpc-2.0:
specifier: ^1.7.0
version: 1.7.0
@@ -51,11 +51,11 @@ importers:
specifier: ^1.2.2
version: 1.2.2
next:
specifier: ^15.5.9
version: 15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^15.5.7
version: 15.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-i18next:
specifier: ^12.1.0
version: 12.1.0(next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 12.1.0(next@15.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ping:
specifier: ^0.4.4
version: 0.4.4
@@ -75,8 +75,8 @@ importers:
specifier: ^15.5.3
version: 15.5.3(i18next@25.5.3(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)
react-icons:
specifier: ^5.5.0
version: 5.5.0(react@18.3.1)
specifier: ^5.4.0
version: 5.4.0(react@18.3.1)
recharts:
specifier: ^3.1.2
version: 3.1.2(@types/react@19.0.10)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@5.0.1)
@@ -90,14 +90,11 @@ importers:
specifier: ^6.0.0
version: 6.0.0
urbackup-server-api:
specifier: ^0.91.0
version: 0.91.0
specifier: ^0.8.9
version: 0.8.9
winston:
specifier: ^3.17.0
version: 3.17.0
ws:
specifier: ^8.18.3
version: 8.18.3
xml-js:
specifier: ^1.6.11
version: 1.6.11
@@ -106,8 +103,8 @@ importers:
specifier: ^0.5.10
version: 0.5.10(tailwindcss@4.0.9)
'@tailwindcss/postcss':
specifier: ^4.1.18
version: 4.1.18
specifier: ^4.1.14
version: 4.1.14
eslint:
specifier: ^9.25.1
version: 9.25.1(jiti@2.6.1)
@@ -469,8 +466,8 @@ packages:
'@napi-rs/wasm-runtime@0.2.8':
resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==}
'@next/env@15.5.9':
resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==}
'@next/env@15.5.7':
resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==}
'@next/eslint-plugin-next@15.2.4':
resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==}
@@ -656,65 +653,65 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1'
'@tailwindcss/node@4.1.18':
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
'@tailwindcss/node@4.1.14':
resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==}
'@tailwindcss/oxide-android-arm64@4.1.18':
resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
'@tailwindcss/oxide-android-arm64@4.1.14':
resolution: {integrity: sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.1.18':
resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
'@tailwindcss/oxide-darwin-arm64@4.1.14':
resolution: {integrity: sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.1.18':
resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
'@tailwindcss/oxide-darwin-x64@4.1.14':
resolution: {integrity: sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.1.18':
resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
'@tailwindcss/oxide-freebsd-x64@4.1.14':
resolution: {integrity: sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14':
resolution: {integrity: sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
'@tailwindcss/oxide-linux-arm64-gnu@4.1.14':
resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
'@tailwindcss/oxide-linux-arm64-musl@4.1.14':
resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
'@tailwindcss/oxide-linux-x64-gnu@4.1.14':
resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
'@tailwindcss/oxide-linux-x64-musl@4.1.14':
resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
'@tailwindcss/oxide-wasm32-wasi@4.1.14':
resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
@@ -725,24 +722,24 @@ packages:
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
'@tailwindcss/oxide-win32-arm64-msvc@4.1.14':
resolution: {integrity: sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
'@tailwindcss/oxide-win32-x64-msvc@4.1.14':
resolution: {integrity: sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.1.18':
resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
'@tailwindcss/oxide@4.1.14':
resolution: {integrity: sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==}
engines: {node: '>= 10'}
'@tailwindcss/postcss@4.1.18':
resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==}
'@tailwindcss/postcss@4.1.14':
resolution: {integrity: sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==}
'@tanstack/react-virtual@3.13.12':
resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
@@ -1038,8 +1035,8 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
async-mutex@0.3.2:
resolution: {integrity: sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
@@ -1123,8 +1120,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001760:
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
caniuse-lite@1.0.30001759:
resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -1323,6 +1320,10 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.1.1:
resolution: {integrity: sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==}
engines: {node: '>=8'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1361,8 +1362,8 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
enhanced-resolve@5.18.4:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
enhanced-resolve@5.18.3:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
es-abstract@1.23.9:
@@ -1965,8 +1966,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
jsep@1.4.0:
@@ -2015,74 +2016,68 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.30.2:
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
lightningcss-darwin-arm64@1.30.1:
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.30.2:
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
lightningcss-darwin-x64@1.30.1:
resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.30.2:
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
lightningcss-freebsd-x64@1.30.1:
resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.30.2:
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
lightningcss-linux-arm-gnueabihf@1.30.1:
resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
lightningcss-linux-arm64-gnu@1.30.1:
resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
lightningcss-linux-arm64-musl@1.30.1:
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
lightningcss-linux-x64-gnu@1.30.1:
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
lightningcss-linux-x64-musl@1.30.1:
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
lightningcss-win32-arm64-msvc@1.30.1:
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.30.2:
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
lightningcss-win32-x64-msvc@1.30.1:
resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.30.2:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
lightningcss@1.30.1:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'}
locate-path@6.0.0:
@@ -2117,8 +2112,8 @@ packages:
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
engines: {node: '>=12'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
@@ -2181,6 +2176,10 @@ packages:
resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==}
engines: {node: '>= 18'}
minizlib@3.1.0:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
@@ -2213,8 +2212,8 @@ packages:
next: '>= 10.0.0'
react: '>= 16.8.0'
next@15.5.9:
resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==}
next@15.5.7:
resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -2459,8 +2458,8 @@ packages:
typescript:
optional: true
react-icons@5.5.0:
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
react-icons@5.4.0:
resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==}
peerDependencies:
react: '*'
@@ -2806,11 +2805,11 @@ packages:
tailwindcss@4.0.9:
resolution: {integrity: sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw==}
tailwindcss@4.1.18:
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
tailwindcss@4.1.14:
resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==}
tapable@2.3.0:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
tapable@2.2.3:
resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==}
engines: {node: '>=6'}
tar-fs@2.1.3:
@@ -2824,6 +2823,10 @@ packages:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
engines: {node: '>=18'}
tar@7.5.1:
resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==}
engines: {node: '>=18'}
telnet-client@2.2.6:
resolution: {integrity: sha512-ZUYrLsPtQupQww3eSEORDVOb6ztdtKEghya6TVXPo2tg/UQq2pn5rHhvwuUvyYpbnsoqdNY1fyD1GNkXHR8dYA==}
@@ -2927,8 +2930,8 @@ packages:
unrs-resolver@1.3.3:
resolution: {integrity: sha512-PFLAGQzYlyjniXdbmQ3dnGMZJXX5yrl2YS4DLRfR3BhgUsE1zpRIrccp9XMOGRfIHpdFvCn/nr5N1KMVda4x3A==}
urbackup-server-api@0.91.0:
resolution: {integrity: sha512-N1CSnGSCSHjwWfGOp6jE56mHYoZor/p++ii8yPsN9P/3cKLBgCvrAZxAbfi+IgK9FZpQEx/kPX1R8OTJRy+x6A==}
urbackup-server-api@0.8.9:
resolution: {integrity: sha512-Igu6A0xSZeMsiN6PWT7zG4aD+iJR5fXT/j5+xwAvnD/vCNfvVrettIsXv6MftxOajvTmtlgaYu8KDoH1EJQ6DQ==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -3014,8 +3017,8 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -3125,7 +3128,7 @@ snapshots:
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.1
js-yaml: 4.1.0
minimatch: 3.1.2
strip-json-comments: 3.1.1
transitivePeerDependencies:
@@ -3348,8 +3351,8 @@ snapshots:
'@types/tar': 6.1.13
'@types/ws': 8.5.14
form-data: 4.0.2
isomorphic-ws: 5.0.0(ws@8.18.3)
js-yaml: 4.1.1
isomorphic-ws: 5.0.0(ws@8.18.0)
js-yaml: 4.1.0
jsonpath-plus: 10.3.0
node-fetch: 2.7.0
openid-client: 6.3.0
@@ -3358,7 +3361,7 @@ snapshots:
tar: 7.4.3
tmp-promise: 3.0.3
tslib: 2.8.1
ws: 8.18.3
ws: 8.18.0
transitivePeerDependencies:
- bufferutil
- encoding
@@ -3371,7 +3374,7 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
'@next/env@15.5.9': {}
'@next/env@15.5.7': {}
'@next/eslint-plugin-next@15.2.4':
dependencies:
@@ -3531,74 +3534,77 @@ snapshots:
mini-svg-data-uri: 1.4.4
tailwindcss: 4.0.9
'@tailwindcss/node@4.1.18':
'@tailwindcss/node@4.1.14':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.18.4
enhanced-resolve: 5.18.3
jiti: 2.6.1
lightningcss: 1.30.2
magic-string: 0.30.21
lightningcss: 1.30.1
magic-string: 0.30.19
source-map-js: 1.2.1
tailwindcss: 4.1.18
tailwindcss: 4.1.14
'@tailwindcss/oxide-android-arm64@4.1.18':
'@tailwindcss/oxide-android-arm64@4.1.14':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.1.18':
'@tailwindcss/oxide-darwin-arm64@4.1.14':
optional: true
'@tailwindcss/oxide-darwin-x64@4.1.18':
'@tailwindcss/oxide-darwin-x64@4.1.14':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.1.18':
'@tailwindcss/oxide-freebsd-x64@4.1.14':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
'@tailwindcss/oxide-linux-arm64-gnu@4.1.14':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
'@tailwindcss/oxide-linux-arm64-musl@4.1.14':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
'@tailwindcss/oxide-linux-x64-gnu@4.1.14':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
'@tailwindcss/oxide-linux-x64-musl@4.1.14':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
'@tailwindcss/oxide-wasm32-wasi@4.1.14':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
'@tailwindcss/oxide-win32-arm64-msvc@4.1.14':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
'@tailwindcss/oxide-win32-x64-msvc@4.1.14':
optional: true
'@tailwindcss/oxide@4.1.18':
'@tailwindcss/oxide@4.1.14':
dependencies:
detect-libc: 2.1.1
tar: 7.5.1
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.1.18
'@tailwindcss/oxide-darwin-arm64': 4.1.18
'@tailwindcss/oxide-darwin-x64': 4.1.18
'@tailwindcss/oxide-freebsd-x64': 4.1.18
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
'@tailwindcss/oxide-linux-arm64-musl': 4.1.18
'@tailwindcss/oxide-linux-x64-gnu': 4.1.18
'@tailwindcss/oxide-linux-x64-musl': 4.1.18
'@tailwindcss/oxide-wasm32-wasi': 4.1.18
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
'@tailwindcss/oxide-android-arm64': 4.1.14
'@tailwindcss/oxide-darwin-arm64': 4.1.14
'@tailwindcss/oxide-darwin-x64': 4.1.14
'@tailwindcss/oxide-freebsd-x64': 4.1.14
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.14
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.14
'@tailwindcss/oxide-linux-arm64-musl': 4.1.14
'@tailwindcss/oxide-linux-x64-gnu': 4.1.14
'@tailwindcss/oxide-linux-x64-musl': 4.1.14
'@tailwindcss/oxide-wasm32-wasi': 4.1.14
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.14
'@tailwindcss/oxide-win32-x64-msvc': 4.1.14
'@tailwindcss/postcss@4.1.18':
'@tailwindcss/postcss@4.1.14':
dependencies:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.1.18
'@tailwindcss/oxide': 4.1.18
'@tailwindcss/node': 4.1.14
'@tailwindcss/oxide': 4.1.14
postcss: 8.5.6
tailwindcss: 4.1.18
tailwindcss: 4.1.14
'@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
@@ -3925,7 +3931,7 @@ snapshots:
async-function@1.0.0: {}
async-mutex@0.5.0:
async-mutex@0.3.2:
dependencies:
tslib: 2.8.1
@@ -4018,7 +4024,7 @@ snapshots:
callsites@3.1.0: {}
caniuse-lite@1.0.30001760: {}
caniuse-lite@1.0.30001759: {}
chalk@4.1.2:
dependencies:
@@ -4194,7 +4200,10 @@ snapshots:
dequal@2.0.3: {}
detect-libc@2.1.2: {}
detect-libc@2.1.1: {}
detect-libc@2.1.2:
optional: true
docker-modem@5.0.6:
dependencies:
@@ -4241,10 +4250,10 @@ snapshots:
dependencies:
once: 1.4.0
enhanced-resolve@5.18.4:
enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.0
tapable: 2.2.3
es-abstract@1.23.9:
dependencies:
@@ -5036,9 +5045,9 @@ snapshots:
isexe@2.0.0: {}
isomorphic-ws@5.0.0(ws@8.18.3):
isomorphic-ws@5.0.0(ws@8.18.0):
dependencies:
ws: 8.18.3
ws: 8.18.0
iterator.prototype@1.1.5:
dependencies:
@@ -5061,7 +5070,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@4.1.1:
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
@@ -5109,54 +5118,50 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lightningcss-android-arm64@1.30.2:
lightningcss-darwin-arm64@1.30.1:
optional: true
lightningcss-darwin-arm64@1.30.2:
lightningcss-darwin-x64@1.30.1:
optional: true
lightningcss-darwin-x64@1.30.2:
lightningcss-freebsd-x64@1.30.1:
optional: true
lightningcss-freebsd-x64@1.30.2:
lightningcss-linux-arm-gnueabihf@1.30.1:
optional: true
lightningcss-linux-arm-gnueabihf@1.30.2:
lightningcss-linux-arm64-gnu@1.30.1:
optional: true
lightningcss-linux-arm64-gnu@1.30.2:
lightningcss-linux-arm64-musl@1.30.1:
optional: true
lightningcss-linux-arm64-musl@1.30.2:
lightningcss-linux-x64-gnu@1.30.1:
optional: true
lightningcss-linux-x64-gnu@1.30.2:
lightningcss-linux-x64-musl@1.30.1:
optional: true
lightningcss-linux-x64-musl@1.30.2:
lightningcss-win32-arm64-msvc@1.30.1:
optional: true
lightningcss-win32-arm64-msvc@1.30.2:
lightningcss-win32-x64-msvc@1.30.1:
optional: true
lightningcss-win32-x64-msvc@1.30.2:
optional: true
lightningcss@1.30.2:
lightningcss@1.30.1:
dependencies:
detect-libc: 2.1.2
detect-libc: 2.1.1
optionalDependencies:
lightningcss-android-arm64: 1.30.2
lightningcss-darwin-arm64: 1.30.2
lightningcss-darwin-x64: 1.30.2
lightningcss-freebsd-x64: 1.30.2
lightningcss-linux-arm-gnueabihf: 1.30.2
lightningcss-linux-arm64-gnu: 1.30.2
lightningcss-linux-arm64-musl: 1.30.2
lightningcss-linux-x64-gnu: 1.30.2
lightningcss-linux-x64-musl: 1.30.2
lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2
lightningcss-darwin-arm64: 1.30.1
lightningcss-darwin-x64: 1.30.1
lightningcss-freebsd-x64: 1.30.1
lightningcss-linux-arm-gnueabihf: 1.30.1
lightningcss-linux-arm64-gnu: 1.30.1
lightningcss-linux-arm64-musl: 1.30.1
lightningcss-linux-x64-gnu: 1.30.1
lightningcss-linux-x64-musl: 1.30.1
lightningcss-win32-arm64-msvc: 1.30.1
lightningcss-win32-x64-msvc: 1.30.1
locate-path@6.0.0:
dependencies:
@@ -5187,7 +5192,7 @@ snapshots:
luxon@3.6.1: {}
magic-string@0.30.21:
magic-string@0.30.19:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -5235,6 +5240,10 @@ snapshots:
minipass: 7.1.2
rimraf: 5.0.10
minizlib@3.1.0:
dependencies:
minipass: 7.1.2
mkdirp-classic@0.5.3: {}
mkdirp@3.0.1: {}
@@ -5250,7 +5259,7 @@ snapshots:
net@1.0.2: {}
next-i18next@12.1.0(next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
next-i18next@12.1.0(next@15.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.9
'@types/hoist-non-react-statics': 3.3.6
@@ -5258,18 +5267,18 @@ snapshots:
hoist-non-react-statics: 3.3.2
i18next: 21.10.0
i18next-fs-backend: 1.2.0
next: 15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next: 15.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-i18next: 11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
transitivePeerDependencies:
- react-dom
- react-native
next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
next@15.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 15.5.9
'@next/env': 15.5.7
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001760
caniuse-lite: 1.0.30001759
postcss: 8.4.31
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -5504,7 +5513,7 @@ snapshots:
react-dom: 18.3.1(react@18.3.1)
typescript: 5.7.3
react-icons@5.5.0(react@18.3.1):
react-icons@5.4.0(react@18.3.1):
dependencies:
react: 18.3.1
@@ -5927,9 +5936,9 @@ snapshots:
tailwindcss@4.0.9: {}
tailwindcss@4.1.18: {}
tailwindcss@4.1.14: {}
tapable@2.3.0: {}
tapable@2.2.3: {}
tar-fs@2.1.3:
dependencies:
@@ -5955,6 +5964,14 @@ snapshots:
mkdirp: 3.0.1
yallist: 5.0.0
tar@7.5.1:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
minipass: 7.1.2
minizlib: 3.1.0
yallist: 5.0.0
telnet-client@2.2.6:
dependencies:
net: 1.0.2
@@ -6080,9 +6097,12 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.3.3
'@unrs/resolver-binding-win32-x64-msvc': 1.3.3
urbackup-server-api@0.91.0:
urbackup-server-api@0.8.9:
dependencies:
async-mutex: 0.5.0
async-mutex: 0.3.2
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
uri-js@4.4.1:
dependencies:
@@ -6218,7 +6238,7 @@ snapshots:
wrappy@1.0.2: {}
ws@8.18.3: {}
ws@8.18.0: {}
xml-js@1.6.11:
dependencies:

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -24,12 +24,12 @@ export default function Resource({
wide ? " min-w-[120px]" : "min-w-[85px]"
}`}
>
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between gap-2">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{value}</div>
<div className="pr-1">{label}</div>
</div>
{expanded && (
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between gap-2">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{expandedValue}</div>
<div className="pr-1">{expandedLabel}</div>
</div>

View File

@@ -59,7 +59,7 @@ export default async function handler(req, res) {
}
if (type === "network") {
let networkData = await si.networkStats("*");
let networkData = await si.networkStats();
let interfaceDefault;
logger.debug("networkData:", JSON.stringify(networkData));
if (interfaceName && interfaceName !== "default") {

View File

@@ -554,38 +554,48 @@ export default function Wrapper({ initialSettings, fallback }) {
html.classList.add(desiredThemeClass);
}
// Remove any previously applied inline styles
body.style.backgroundImage = "";
body.style.backgroundColor = "";
body.style.backgroundAttachment = "";
if (backgroundImage) {
const safeBackgroundImage = backgroundImage.replace(/'/g, "\\'");
body.style.backgroundImage = `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${safeBackgroundImage}')`;
body.style.backgroundSize = "cover";
body.style.backgroundPosition = "center";
body.style.backgroundAttachment = "fixed";
body.style.backgroundRepeat = "no-repeat";
body.style.backgroundColor = "";
} else {
body.style.backgroundImage = "none";
body.style.backgroundColor = "rgb(var(--bg-color))";
body.style.backgroundSize = "";
body.style.backgroundPosition = "";
body.style.backgroundAttachment = "";
body.style.backgroundRepeat = "";
}
return () => {
body.style.backgroundImage = "";
body.style.backgroundColor = "";
body.style.backgroundSize = "";
body.style.backgroundPosition = "";
body.style.backgroundAttachment = "";
body.style.backgroundRepeat = "";
};
}, [backgroundImage, opacity, theme, color, initialSettings.color]);
return (
<>
{backgroundImage && (
<div
id="background"
aria-hidden="true"
style={{
backgroundImage: `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${backgroundImage}')`,
}}
/>
)}
<div id="page_wrapper" className="relative h-full">
<div
id="inner_wrapper"
tabIndex="-1"
className={classNames(
"w-full h-full overflow-auto",
backgroundBlur &&
`backdrop-blur${initialSettings.background.blur?.length ? `-${initialSettings.background.blur}` : ""}`,
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
)}
>
<Index initialSettings={initialSettings} fallback={fallback} />
</div>
<div id="page_wrapper" className="relative min-h-screen">
<div
id="inner_wrapper"
tabIndex="-1"
className={classNames(
"w-full min-h-screen overflow-auto",
backgroundBlur &&
`backdrop-blur${initialSettings.background.blur?.length ? `-${initialSettings.background.blur}` : ""}`,
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
)}
>
<Index initialSettings={initialSettings} fallback={fallback} />
</div>
</>
</div>
);
}

View File

@@ -30,18 +30,6 @@ body,
height: 100%;
margin: 0;
padding: 0;
background-color: rgb(var(--bg-color));
}
#background {
position: fixed;
inset: 0;
z-index: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: scroll;
pointer-events: none;
}
html,

View File

@@ -35,7 +35,7 @@ function parseServicesToGroups(services) {
serviceGroupServices.push({
name: entryName,
...entries[entryName],
weight: entries[entryName].weight ?? (serviceGroupServices.length + 1) * 100, // default weight
weight: entries[entryName].weight || serviceGroupServices.length * 100, // default weight
type: "service",
});
}
@@ -107,7 +107,6 @@ export async function servicesFromDocker() {
constructedService = {
container: containerName.replace(/^\//, ""),
server: serverName,
weight: 0,
type: "service",
};
}

View File

@@ -27,9 +27,6 @@ export default async function credentialedProxyHandler(req, res, map) {
const headers = {
"Content-Type": "application/json",
...(widgets[widget.type].headers ?? {}),
...(widget.headers ?? {}),
...(req.extraHeaders ?? {}),
};
if (widget.type === "stocks") {

View File

@@ -25,11 +25,7 @@ export default async function genericProxyHandler(req, res, map) {
}
const url = new URL(urlString);
const headers = {
...(widgets[widget.type].headers ?? {}),
...(widget.headers ?? {}),
...(req.extraHeaders ?? {}),
};
const headers = req.extraHeaders ?? widget.headers ?? widgets[widget.type].headers ?? {};
if (widget.username && widget.password) {
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;

View File

@@ -85,9 +85,6 @@ export default async function fritzboxProxyHandler(req, res) {
requestExternalIPv6Prefix ? requestEndpoint(apiBaseUrl, "WANIPConnection", "X_AVM_DE_GetIPv6Prefix") : null,
])
.then(([statusInfo, linkProperties, addonInfos, externalIPAddress, externalIPv6Address, externalIPv6Prefix]) => {
const ipv6Prefix = externalIPv6Prefix?.NewIPv6Prefix;
const ipv6Len = externalIPv6Prefix?.NewPrefixLength;
res.status(200).json({
connectionStatus: statusInfo?.NewConnectionStatus || "Unconfigured",
uptime: statusInfo?.NewUptime || 0,
@@ -99,7 +96,7 @@ export default async function fritzboxProxyHandler(req, res) {
sent: addonInfos?.NewX_AVM_DE_TotalBytesSent64 || 0,
externalIPAddress: externalIPAddress?.NewExternalIPAddress || null,
externalIPv6Address: externalIPv6Address?.NewExternalIPv6Address || null,
externalIPv6Prefix: ipv6Prefix && ipv6Len != null ? `${ipv6Prefix}/${ipv6Len}` : (ipv6Prefix ?? null),
externalIPv6Prefix: externalIPv6Prefix?.NewIPv6Prefix || null,
});
})
.catch((error) => {

View File

@@ -3,12 +3,13 @@ import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export const jellyseerrDefaultFields = ["pending", "approved", "available"];
export const jellyseerrDefaultFields = ["pending", "approved", "completed"];
const MAX_ALLOWED_FIELDS = 4;
export default function Component({ service }) {
const { widget } = service;
widget.fields = widget?.fields?.length ? widget.fields : jellyseerrDefaultFields;
widget.fields = widget?.fields?.length ? widget.fields.slice(0, MAX_ALLOWED_FIELDS) : jellyseerrDefaultFields;
const isIssueEnabled = widget.fields.includes("issues");
const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
@@ -23,16 +24,24 @@ export default function Component({ service }) {
<Block label="jellyseerr.pending" />
<Block label="jellyseerr.approved" />
<Block label="jellyseerr.available" />
<Block label="jellyseerr.completed" />
<Block label="jellyseerr.issues" />
</Container>
);
}
if (statsData.completed === undefined) {
// Newer versions added "completed", fallback to available
widget.fields = widget.fields.filter((field) => field !== "completed");
widget.fields.push("available");
}
return (
<Container service={service}>
<Block label="jellyseerr.pending" value={statsData.pending} />
<Block label="jellyseerr.approved" value={statsData.approved} />
<Block label="jellyseerr.available" value={statsData.available} />
<Block label="jellyseerr.completed" value={statsData.completed} />
<Block label="jellyseerr.issues" value={`${issueData?.open} / ${issueData?.total}`} />
</Container>
);

View File

@@ -65,7 +65,7 @@ async function fetchFromPlexAPI(endpoint, widget) {
export default async function plexProxyHandler(req, res) {
const widget = await getWidget(req);
const { service, index } = req.query;
const { service } = req.query;
if (!widget) {
return res.status(400).json({ error: "Invalid proxy service type" });
@@ -85,19 +85,19 @@ export default async function plexProxyHandler(req, res) {
streams = apiData.MediaContainer._attributes.size;
}
let libraries = cache.get(`${librariesCacheKey}.${service}.${index}`);
let libraries = cache.get(`${librariesCacheKey}.${service}`);
if (libraries === null) {
logger.debug("Getting libraries from Plex API");
[status, apiData] = await fetchFromPlexAPI("/library/sections", widget);
if (apiData && apiData.MediaContainer) {
libraries = [].concat(apiData.MediaContainer.Directory);
cache.put(`${librariesCacheKey}.${service}.${index}`, libraries, 1000 * 60 * 60 * 6);
cache.put(`${librariesCacheKey}.${service}`, libraries, 1000 * 60 * 60 * 6);
}
}
let albums = cache.get(`${albumsCacheKey}.${service}.${index}`);
let movies = cache.get(`${moviesCacheKey}.${service}.${index}`);
let tv = cache.get(`${tvCacheKey}.${service}.${index}`);
let albums = cache.get(`${albumsCacheKey}.${service}`);
let movies = cache.get(`${moviesCacheKey}.${service}`);
let tv = cache.get(`${tvCacheKey}.${service}`);
if (albums === null || movies === null || tv === null) {
albums = 0;
movies = 0;
@@ -123,9 +123,9 @@ export default async function plexProxyHandler(req, res) {
}
}),
);
cache.put(`${albumsCacheKey}.${service}.${index}`, albums, 1000 * 60 * 10);
cache.put(`${tvCacheKey}.${service}.${index}`, tv, 1000 * 60 * 10);
cache.put(`${moviesCacheKey}.${service}.${index}`, movies, 1000 * 60 * 10);
cache.put(`${albumsCacheKey}.${service}`, albums, 1000 * 60 * 10);
cache.put(`${tvCacheKey}.${service}`, tv, 1000 * 60 * 10);
cache.put(`${moviesCacheKey}.${service}`, movies, 1000 * 60 * 10);
}
const data = {

View File

@@ -11,15 +11,6 @@ const logger = createLogger(proxyName);
const sessionCacheKey = `${proxyName}__sessionId`;
const isNgCacheKey = `${proxyName}__isNg`;
function parsePyloadResponse(url, data) {
try {
return JSON.parse(Buffer.from(data).toString());
} catch (e) {
logger.error(`Error communicating with pyload API at ${url}, returned: ${JSON.stringify(data)}`);
return data;
}
}
async function fetchFromPyloadAPI(url, sessionId, params, service) {
const options = {
body: params
@@ -42,33 +33,13 @@ async function fetchFromPyloadAPI(url, sessionId, params, service) {
// eslint-disable-next-line no-unused-vars
const [status, contentType, data, responseHeaders] = await httpProxy(url, options);
const returnData = parsePyloadResponse(url, data);
return [status, returnData, responseHeaders];
}
async function fetchFromPyloadAPIBasic(url, params, username, password) {
const parsedUrl = new URL(url);
const isGetRequest = !params || Object.keys(params).length === 0;
const options = {
method: isGetRequest ? "GET" : "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
},
};
if (isGetRequest) {
if (params) {
Object.keys(params).forEach((key) => parsedUrl.searchParams.append(key, params[key]));
}
} else {
options.headers["Content-Type"] = "application/json";
options.body = JSON.stringify(params);
let returnData;
try {
returnData = JSON.parse(Buffer.from(data).toString());
} catch (e) {
logger.error(`Error communicating with pyload API at ${url}, returned: ${JSON.stringify(data)}`);
returnData = data;
}
// eslint-disable-next-line no-unused-vars
const [status, contentType, data, responseHeaders] = await httpProxy(parsedUrl, options);
const returnData = parsePyloadResponse(parsedUrl, data);
return [status, returnData, responseHeaders];
}
@@ -95,43 +66,24 @@ async function login(loginUrl, service, username, password = "") {
return sessionId;
}
export default async function pyloadProxyHandler(req, res, map = {}) {
export default async function pyloadProxyHandler(req, res) {
const { group, service, endpoint, index } = req.query;
const { ngEndpoint } = map;
try {
if (group && service) {
const widget = await getServiceWidget(group, service, index);
if (widget) {
const apiTemplate = widgets[widget.type].api;
const url = new URL(formatApiCall(apiTemplate, { endpoint, ...widget }));
const ngUrl = ngEndpoint ? new URL(formatApiCall(apiTemplate, { endpoint: ngEndpoint, ...widget })) : url;
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const loginUrl = `${widget.url}/api/login`;
const hasCredentials = widget.username && widget.password;
if (hasCredentials) {
const [status, data] = await fetchFromPyloadAPIBasic(ngUrl, null, widget.username, widget.password);
if (status === 200 && !data?.error) {
cache.put(`${isNgCacheKey}.${service}`, true);
return res.json(data);
}
if (status === 401) {
return res
.status(status)
.send({ error: { message: "Invalid credentials communicating with Pyload API", data } });
}
}
let sessionId =
cache.get(`${sessionCacheKey}.${service}`) ??
(await login(loginUrl, service, widget.username, widget.password));
let [status, data] = await fetchFromPyloadAPI(url, sessionId, null, service);
if (status === 403 || status === 401 || (status === 400 && data?.error?.includes("CSRF token"))) {
logger.info("Failed to retrieve data from Pyload API with session auth, trying to login again...");
if (status === 403 || status === 401) {
logger.info("Failed to retrieve data from Pyload API, trying to login again...");
cache.del(`${sessionCacheKey}.${service}`);
sessionId = await login(loginUrl, service, widget.username, widget.password);
[status, data] = await fetchFromPyloadAPI(url, sessionId, null, service);

View File

@@ -7,7 +7,6 @@ const widget = {
mappings: {
status: {
endpoint: "statusServer",
map: { ngEndpoint: "status_server" },
},
},
};

View File

@@ -12,8 +12,8 @@ export default function Component({ service }) {
const { data: alertData, error: alertError } = useWidgetAPI(widget, "alerts");
const { data: statusData, error: statusError } = useWidgetAPI(widget, "status");
const { data: poolsData, error: poolsError } = useWidgetAPI(widget, widget?.enablePools ? "pools" : "");
const { data: datasetData, error: datasetError } = useWidgetAPI(widget, widget?.enablePools ? "dataset" : "");
const { data: poolsData, error: poolsError } = useWidgetAPI(widget, widget?.enablePools ? "pools" : null);
const { data: datasetData, error: datasetError } = useWidgetAPI(widget, widget?.enablePools ? "dataset" : null);
if (alertError || statusError || poolsError) {
const finalError = alertError ?? statusError ?? poolsError ?? datasetError;

View File

@@ -1,172 +0,0 @@
import WebSocket from "ws";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
import validateWidgetData from "utils/proxy/validate-widget-data";
import widgets from "widgets/widgets";
const logger = createLogger("truenasProxyHandler");
function waitForEvent(ws, handler, { event = "message", parseJson = true } = {}) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
cleanup();
reject(new Error("TrueNAS websocket wait timed out"));
}, 10000);
const handleEvent = (payload) => {
try {
let parsed = payload;
if (parseJson) {
if (Buffer.isBuffer(payload)) {
parsed = JSON.parse(payload.toString());
} else if (typeof payload === "string") {
parsed = JSON.parse(payload);
}
logger.info("Received TrueNAS websocket message: %o", parsed);
} else {
logger.info("Received TrueNAS websocket message: %o", payload);
}
const handlerResult = handler(parsed);
if (handlerResult !== undefined) {
cleanup();
if (handlerResult instanceof Error) {
reject(handlerResult);
} else {
resolve(handlerResult);
}
}
} catch (err) {
cleanup();
reject(err);
}
};
const handleError = (err) => {
cleanup();
logger.error("TrueNAS websocket error: %s", err?.message ?? err);
reject(err);
};
const handleClose = () => {
cleanup();
logger.error("TrueNAS websocket connection closed unexpectedly");
reject(new Error("TrueNAS websocket closed the connection"));
};
function cleanup() {
clearTimeout(timeout);
ws.off(event, handleEvent);
ws.off("error", handleError);
ws.off("close", handleClose);
}
ws.on(event, handleEvent);
ws.on("error", handleError);
ws.on("close", handleClose);
});
}
let nextId = 1;
async function sendMethod(ws, method, params = []) {
const id = nextId++;
const payload = { jsonrpc: "2.0", id, method, params };
logger.info("Sending TrueNAS websocket method %s with id %d", method, id);
ws.send(JSON.stringify(payload));
return waitForEvent(ws, (message) => {
if (message?.id !== id) return undefined;
if (message?.error) {
return new Error(message.error?.message || JSON.stringify(message.error));
}
return message?.result ?? message;
});
}
async function authenticate(ws, widget) {
if (widget?.key) {
try {
const apiKeyResult = await sendMethod(ws, "auth.login_with_api_key", [widget.key]);
if (apiKeyResult === true) return;
logger.warn("TrueNAS API key authentication failed, falling back to username/password when available.");
} catch (err) {
logger.warn("TrueNAS API key authentication failed: %s", err?.message ?? err);
}
}
if (widget?.username && widget?.password) {
const loginResult = await sendMethod(ws, "auth.login", [widget.username, widget.password]);
if (loginResult === true) return;
logger.warn("TrueNAS username/password authentication failed.");
}
throw new Error("TrueNAS authentication failed");
}
export default async function truenasProxyHandler(req, res, map) {
const { group, service, endpoint, index } = req.query;
if (!group || !service) {
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
return res.status(400).json({ error: "Invalid proxy service type" });
}
const widget = await getServiceWidget(group, service, index);
if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return res.status(400).json({ error: "Invalid proxy service type" });
}
if (!endpoint) {
return res.status(204).end();
}
const version = Number(widget.version ?? 1);
if (Number.isNaN(version) || version < 2) {
// Use legacy REST proxy for version 1
return credentialedProxyHandler(req, res, map);
}
const mappingEntry = Object.values(widgets[widget.type].mappings).find((mapping) => mapping.endpoint === endpoint);
const wsMethod = mappingEntry.wsMethod;
if (!wsMethod) {
logger.debug("Missing wsMethod mapping for TrueNAS endpoint %s", endpoint);
return res.status(500).json({ error: "Missing wsMethod mapping." });
}
try {
let data;
const wsUrl = new URL(formatApiCall(widgets[widget.type].wsAPI, { ...widget }));
const useSecure = wsUrl.protocol === "https:" || Boolean(widget.key); // API key requires secure connection
if (useSecure && wsUrl.protocol !== "https:")
logger.info("Upgrading TrueNAS websocket connection to secure wss://");
wsUrl.protocol = useSecure ? "wss:" : "ws:";
logger.info("Connecting to TrueNAS websocket at %s", wsUrl);
const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
await waitForEvent(ws, () => true, { event: "open", parseJson: false }); // wait for open
logger.info("Connected to TrueNAS websocket at %s", wsUrl);
try {
await authenticate(ws, widget);
data = await sendMethod(ws, wsMethod);
} finally {
ws.close();
}
if (!validateWidgetData(widget, endpoint, data)) {
return res.status(500).json({ error: { message: "Invalid data", url: sanitizeErrorURL(widget.url), data } });
}
if (map) data = map(data);
return res.status(200).json(data);
} catch (err) {
if (err?.status) {
return res.status(err.status).json({ error: err.message });
}
logger.warn("Websocket call for TrueNAS failed: %s", err?.message ?? err);
return res.status(500).json({ error: err?.message ?? "TrueNAS websocket call failed" });
}
}

View File

@@ -1,43 +1,32 @@
import truenasProxyHandler from "./proxy";
import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v2.0/{endpoint}",
wsAPI: "{url}/api/current",
proxyHandler: truenasProxyHandler,
proxyHandler: credentialedProxyHandler,
mappings: {
alerts: {
endpoint: "alert/list",
wsMethod: "alert.list",
map: (data) => {
if (Array.isArray(data)) {
return { pending: data.filter((item) => item?.dismissed === false).length };
}
return { pending: jsonArrayFilter(data, (item) => item?.dismissed === false).length };
},
map: (data) => ({
pending: jsonArrayFilter(data, (item) => item?.dismissed === false).length,
}),
},
status: {
endpoint: "system/info",
wsMethod: "system.info",
validate: ["loadavg", "uptime_seconds"],
},
pools: {
endpoint: "pool",
wsMethod: "pool.query",
map: (data) => {
const list = Array.isArray(data) ? data : asJson(data);
return list.map((entry) => ({
map: (data) =>
asJson(data).map((entry) => ({
id: entry.name,
name: entry.name,
healthy: entry.healthy,
}));
},
})),
},
dataset: {
endpoint: "pool/dataset",
wsMethod: "pool.dataset.query",
},
},
};

View File

@@ -49,12 +49,10 @@ export default function Component({ service }) {
// single monitor
const monitor = uptimerobotData.monitors[0];
const logs = Array.isArray(monitor.logs) ? monitor.logs : [];
const lastUpLog = logs.find((log) => log.type === 2);
const lastDownLog = logs.find((log) => log.type === 1);
let status;
let uptime = 0;
let logIndex = 0;
const hasLogs = Array.isArray(monitor.logs) && monitor.logs.length > 0;
switch (monitor.status) {
case 0:
@@ -65,7 +63,8 @@ export default function Component({ service }) {
break;
case 2:
status = t("uptimerobot.up");
uptime = t("common.duration", { value: lastUpLog?.duration ?? 0 });
uptime = t("common.duration", { value: hasLogs ? monitor.logs[0].duration : 0 });
logIndex = 1;
break;
case 8:
status = t("uptimerobot.seemsdown");
@@ -78,14 +77,14 @@ export default function Component({ service }) {
break;
}
const lastDown = lastDownLog ? new Date(lastDownLog.datetime * 1000).toLocaleString() : "";
const downDuration = t("common.duration", { value: lastDownLog?.duration ?? 0 });
const hideDown = !lastDownLog;
const lastDown = hasLogs ? new Date(monitor.logs[logIndex].datetime * 1000).toLocaleString() : "";
const downDuration = t("common.duration", { value: hasLogs ? monitor.logs[logIndex].duration : 0 });
const hideDown = !hasLogs || (logIndex === 1 && monitor.logs[logIndex].type !== 1);
return (
<Container service={service}>
<Block label="uptimerobot.status" value={status} />
<Block label="uptimerobot.uptime" value={uptime} />
{hasLogs && <Block label="uptimerobot.uptime" value={uptime} />}
{!hideDown && <Block label="uptimerobot.lastDown" value={lastDown} />}
{!hideDown && <Block label="uptimerobot.downDuration" value={downDuration} />}
</Container>

View File

@@ -6,17 +6,8 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
const MAX_ALLOWED_FIELDS = 4;
const todayDate = new Date();
function toApiMonthYear(offset = 0) {
// API expects 1-indexed months, wrap around if needed
const m = todayDate.getMonth() + 1 + offset;
return {
month: ((m + 11) % 12) + 1,
year: todayDate.getFullYear() + Math.floor((m - 1) / 12),
};
}
export default function Component({ service }) {
const todayDate = new Date();
const { t } = useTranslation();
const { widget } = service;
@@ -38,19 +29,28 @@ export default function Component({ service }) {
const { data: subscriptionsThisMonthlyCostData, error: subscriptionsThisMonthlyCostError } = useWidgetAPI(
widget,
subscriptionsThisMonthlyEndpoint,
toApiMonthYear(), // this month
{
month: todayDate.getMonth(),
year: todayDate.getFullYear(),
},
);
const subscriptionsNextMonthlyEndpoint = widget.fields.includes("nextMonthlyCost") ? "get_monthly_cost" : "";
const { data: subscriptionsNextMonthlyCostData, error: subscriptionsNextMonthlyCostError } = useWidgetAPI(
widget,
subscriptionsNextMonthlyEndpoint,
toApiMonthYear(1), // next month
{
month: todayDate.getMonth() + 1,
year: todayDate.getFullYear(),
},
);
const subscriptionsPreviousMonthlyEndpoint = widget.fields.includes("previousMonthlyCost") ? "get_monthly_cost" : "";
const { data: subscriptionsPreviousMonthlyCostData, error: subscriptionsPreviousMonthlyCostError } = useWidgetAPI(
widget,
subscriptionsPreviousMonthlyEndpoint,
toApiMonthYear(-1), // previous month
{
month: todayDate.getMonth() - 1,
year: todayDate.getFullYear(),
},
);
if (