Compare commits

...

14 Commits

Author SHA1 Message Date
Crowdin Bot
8bba944441 New Crowdin translations by GitHub Action 2026-01-01 12:15:28 +00:00
shamoon
08da8e66fd Add @tailwindcss/oxide to onlyBuiltDependencies
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
2025-12-30 19:24:42 -08:00
shamoon
682e0cbc82 Enhancement: Add support for Pyload 0.5.0 CSRF-protected API (#6142) 2025-12-30 19:18:56 -08:00
shamoon
f7ad322d4c Revert "Fix: restore bg image to body again (#5828)"
Some checks failed
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
This reverts commit 06cf76d724.
2025-12-30 19:01:08 -08:00
shamoon
2b31c23b9e Fix: support latest homebridge status labels (#6139)
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 / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
2025-12-29 14:15:42 -08:00
Daniel
ae258b8276 Fix: ensure minimum gap for resource widget items (#6137) 2025-12-29 14:03:09 -08:00
I-am-not-a-number
ff296be4a4 Enhancement: include prefix length when displaying ipv6 prefix (#6130)
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (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
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-12-27 17:44:30 +00:00
Cameron Horn
31da9ee417 Fix: prevent cache collision with multiple plex widgets (#6126)
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (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
2025-12-24 14:47:22 -08:00
shamoon
be7a00d631 Enhancement: fully support custom headers (#6125)
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (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
2025-12-23 08:02:58 -08:00
shamoon
0d99a8766f Fix: retrieve stats from all network interfaces (#6102)
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (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
2025-12-15 14:05:00 -08:00
dependabot[bot]
e66b58dc53 Chore(deps): Bump next from 15.5.7 to 15.5.9 (#6089)
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (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
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 23:17:21 +00:00
shamoon
1b32cbbbfd Enhancement: refactor UptimeRobot widget (#6088) 2025-12-12 15:04:22 -08:00
dependabot[bot]
681a8a828b Chore(deps): Bump actions/cache from 4 to 5 (#6085)
Some checks failed
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
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
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 16:22:01 +00:00
dependabot[bot]
f8009a7067 Chore(deps): Bump dessant/lock-threads from 5 to 6 (#6084)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 08:12:51 -08:00
62 changed files with 303 additions and 175 deletions

View File

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

View File

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

View File

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

View File

@@ -189,6 +189,8 @@ labels: ...
- homepage.widgets[1].slug=youreventslughere - 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: You can add specify fields for e.g. the [CustomAPI](../widgets/services/customapi.md) widget by using array-style dot notation:
```yaml ```yaml

View File

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

View File

@@ -101,6 +101,25 @@ Each service can have multiple widgets attached to it, for example:
Multiple widgets per service are not yet supported with Kubernetes ingress annotations. 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 #### 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. 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

@@ -25,7 +25,7 @@
"luxon": "^3.6.1", "luxon": "^3.6.1",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"minecraftstatuspinger": "^1.2.2", "minecraftstatuspinger": "^1.2.2",
"next": "^15.5.7", "next": "^15.5.9",
"next-i18next": "^12.1.0", "next-i18next": "^12.1.0",
"ping": "^0.4.4", "ping": "^0.4.4",
"pretty-bytes": "^7.1.0", "pretty-bytes": "^7.1.0",
@@ -65,6 +65,7 @@
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@tailwindcss/oxide",
"osx-temperature-sensor", "osx-temperature-sensor",
"sharp" "sharp"
] ]

32
pnpm-lock.yaml generated
View File

@@ -51,11 +51,11 @@ importers:
specifier: ^1.2.2 specifier: ^1.2.2
version: 1.2.2 version: 1.2.2
next: next:
specifier: ^15.5.7 specifier: ^15.5.9
version: 15.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-i18next: next-i18next:
specifier: ^12.1.0 specifier: ^12.1.0
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) 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)
ping: ping:
specifier: ^0.4.4 specifier: ^0.4.4
version: 0.4.4 version: 0.4.4
@@ -466,8 +466,8 @@ packages:
'@napi-rs/wasm-runtime@0.2.8': '@napi-rs/wasm-runtime@0.2.8':
resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==} resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==}
'@next/env@15.5.7': '@next/env@15.5.9':
resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==} resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==}
'@next/eslint-plugin-next@15.2.4': '@next/eslint-plugin-next@15.2.4':
resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==} resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==}
@@ -1120,8 +1120,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
caniuse-lite@1.0.30001759: caniuse-lite@1.0.30001760:
resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
chalk@4.1.2: chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -2212,8 +2212,8 @@ packages:
next: '>= 10.0.0' next: '>= 10.0.0'
react: '>= 16.8.0' react: '>= 16.8.0'
next@15.5.7: next@15.5.9:
resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==} resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -3374,7 +3374,7 @@ snapshots:
'@tybys/wasm-util': 0.9.0 '@tybys/wasm-util': 0.9.0
optional: true optional: true
'@next/env@15.5.7': {} '@next/env@15.5.9': {}
'@next/eslint-plugin-next@15.2.4': '@next/eslint-plugin-next@15.2.4':
dependencies: dependencies:
@@ -4024,7 +4024,7 @@ snapshots:
callsites@3.1.0: {} callsites@3.1.0: {}
caniuse-lite@1.0.30001759: {} caniuse-lite@1.0.30001760: {}
chalk@4.1.2: chalk@4.1.2:
dependencies: dependencies:
@@ -5259,7 +5259,7 @@ snapshots:
net@1.0.2: {} net@1.0.2: {}
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): 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):
dependencies: dependencies:
'@babel/runtime': 7.26.9 '@babel/runtime': 7.26.9
'@types/hoist-non-react-statics': 3.3.6 '@types/hoist-non-react-statics': 3.3.6
@@ -5267,18 +5267,18 @@ snapshots:
hoist-non-react-statics: 3.3.2 hoist-non-react-statics: 3.3.2
i18next: 21.10.0 i18next: 21.10.0
i18next-fs-backend: 1.2.0 i18next-fs-backend: 1.2.0
next: 15.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: 15.5.9(react-dom@18.3.1(react@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) react-i18next: 11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
transitivePeerDependencies: transitivePeerDependencies:
- react-dom - react-dom
- react-native - react-native
next@15.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@next/env': 15.5.7 '@next/env': 15.5.9
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001759 caniuse-lite: 1.0.30001760
postcss: 8.4.31 postcss: 8.4.31
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Op", "up": "Op",
"pending": "Afwagtend", "pending": "Afwagtend",
"down": "Af" "down": "Af",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Nuut", "new": "Nuut",

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Up", "up": "Up",
"pending": "Pending", "pending": "Pending",
"down": "Down" "down": "Down",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "جديد(ة)", "new": "جديد(ة)",

View File

@@ -63,7 +63,7 @@
"wlan_users": "WLAN Потребители", "wlan_users": "WLAN Потребители",
"up": "UP", "up": "UP",
"down": "DOWN", "down": "DOWN",
"wait": "Please wait", "wait": "Моля изчакайте",
"empty_data": "Неизвестен статус на подсистема" "empty_data": "Неизвестен статус на подсистема"
}, },
"docker": { "docker": {
@@ -83,7 +83,7 @@
"partial": "Частично" "partial": "Частично"
}, },
"ping": { "ping": {
"error": "Error", "error": "Грешка",
"ping": "Пинг", "ping": "Пинг",
"down": "Down", "down": "Down",
"up": "Up", "up": "Up",
@@ -91,11 +91,11 @@
}, },
"siteMonitor": { "siteMonitor": {
"http_status": "HTTP статус", "http_status": "HTTP статус",
"error": "Error", "error": "Грешка",
"response": "Отговор", "response": "Отговор",
"down": "Down", "down": "Down",
"up": "Up", "up": "Up",
"not_available": "Not Available" "not_available": "Не е налично"
}, },
"emby": { "emby": {
"playing": "Възпроизвежда", "playing": "Възпроизвежда",
@@ -111,7 +111,7 @@
"offline": "Offline", "offline": "Offline",
"offline_alt": "Offline", "offline_alt": "Offline",
"online": "Онлайн", "online": "Онлайн",
"total": "Total", "total": "Общо",
"unknown": "Unknown" "unknown": "Unknown"
}, },
"evcc": { "evcc": {
@@ -133,7 +133,7 @@
"unread": "Непрочетени" "unread": "Непрочетени"
}, },
"fritzbox": { "fritzbox": {
"connectionStatus": "Status", "connectionStatus": "Статус",
"connectionStatusUnconfigured": "Неконфигуриран", "connectionStatusUnconfigured": "Неконфигуриран",
"connectionStatusConnecting": "Свързване", "connectionStatusConnecting": "Свързване",
"connectionStatusAuthenticating": "Удостоверяване", "connectionStatusAuthenticating": "Удостоверяване",
@@ -141,7 +141,7 @@
"connectionStatusDisconnecting": "Прекъсване на връзката", "connectionStatusDisconnecting": "Прекъсване на връзката",
"connectionStatusDisconnected": "Не е свързан", "connectionStatusDisconnected": "Не е свързан",
"connectionStatusConnected": "Свързан", "connectionStatusConnected": "Свързан",
"uptime": "Uptime", "uptime": "Време на работа",
"maxDown": "Макс сваляне", "maxDown": "Макс сваляне",
"maxUp": "Макс качване", "maxUp": "Макс качване",
"down": "Down", "down": "Down",
@@ -170,8 +170,8 @@
"tautulli": { "tautulli": {
"playing": "Playing", "playing": "Playing",
"transcoding": "Transcoding", "transcoding": "Transcoding",
"bitrate": "Bitrate", "bitrate": "Битрейт",
"no_active": "No Active Streams", "no_active": "Няма активни потоци",
"plex_connection_error": "Провери връзка с Plex" "plex_connection_error": "Провери връзка с Plex"
}, },
"omada": { "omada": {
@@ -189,7 +189,7 @@
"plex": { "plex": {
"streams": "Активни Потоци", "streams": "Активни Потоци",
"albums": "Албуми", "albums": "Албуми",
"movies": "Movies", "movies": "Филми",
"tv": "Сериали" "tv": "Сериали"
}, },
"sabnzbd": { "sabnzbd": {
@@ -362,8 +362,8 @@
}, },
"trilium": { "trilium": {
"version": "Version", "version": "Version",
"notesCount": "Notes", "notesCount": "Бележки",
"dbSize": "Database Size", "dbSize": "Размер на базата данни",
"unknown": "Unknown" "unknown": "Unknown"
}, },
"navidrome": { "navidrome": {
@@ -373,7 +373,7 @@
"npm": { "npm": {
"enabled": "Активирано", "enabled": "Активирано",
"disabled": "Деактивирано", "disabled": "Деактивирано",
"total": "Total" "total": "Общо"
}, },
"coinmarketcap": { "coinmarketcap": {
"configure": "Настрой за следене една или повече крипто валути", "configure": "Настрой за следене една или повече крипто валути",
@@ -384,7 +384,7 @@
}, },
"gotify": { "gotify": {
"apps": "Приложения", "apps": "Приложения",
"clients": "Clients", "clients": "Клиенти",
"messages": "Съобщения" "messages": "Съобщения"
}, },
"prowlarr": { "prowlarr": {
@@ -405,7 +405,7 @@
"transferRate": "Rate" "transferRate": "Rate"
}, },
"mastodon": { "mastodon": {
"user_count": "Users", "user_count": "Потребители",
"status_count": "Posts", "status_count": "Posts",
"domain_count": "Domains" "domain_count": "Domains"
}, },
@@ -416,17 +416,17 @@
}, },
"minecraft": { "minecraft": {
"players": "Играчи", "players": "Играчи",
"version": "Version", "version": "Версия",
"status": "Status", "status": "Статус",
"up": "Online", "up": "Онлайн",
"down": "Offline" "down": "Офлайн"
}, },
"miniflux": { "miniflux": {
"read": "Read", "read": "Read",
"unread": "Unread" "unread": "Unread"
}, },
"authentik": { "authentik": {
"users": "Users", "users": "Потребители",
"loginsLast24H": "Logins (24h)", "loginsLast24H": "Logins (24h)",
"failedLoginsLast24H": "Failed Logins (24h)" "failedLoginsLast24H": "Failed Logins (24h)"
}, },
@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Up", "up": "Up",
"pending": "Pending", "pending": "Pending",
"down": "Down" "down": "Down",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Нови", "new": "Нови",

View File

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

View File

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

View File

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

View File

@@ -45,9 +45,9 @@
"free": "Frei", "free": "Frei",
"used": "In Benutzung", "used": "In Benutzung",
"load": "Last", "load": "Last",
"temp": "TEMP", "temp": "Temp",
"max": "Max", "max": "Max",
"uptime": "BETRIEBSZEIT" "uptime": "Betriebszeit"
}, },
"unifi": { "unifi": {
"users": "Benutzer", "users": "Benutzer",
@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Online", "up": "Online",
"pending": "Wartend", "pending": "Wartend",
"down": "Offline" "down": "Offline",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Neu", "new": "Neu",
@@ -602,7 +603,7 @@
"pangolin": { "pangolin": {
"orgs": "Orgs", "orgs": "Orgs",
"sites": "Sites", "sites": "Sites",
"resources": "Resources", "resources": "Ressourcen",
"targets": "Targets", "targets": "Targets",
"traffic": "Traffic", "traffic": "Traffic",
"in": "In", "in": "In",

View File

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

View File

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

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Activo", "up": "Activo",
"pending": "Pendiente", "pending": "Pendiente",
"down": "Inactivo" "down": "Inactivo",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Nuevo", "new": "Nuevo",
@@ -601,12 +602,12 @@
}, },
"pangolin": { "pangolin": {
"orgs": "Orgs", "orgs": "Orgs",
"sites": "Sites", "sites": "Sitios",
"resources": "Resources", "resources": "Recursos",
"targets": "Targets", "targets": "Destinos",
"traffic": "Traffic", "traffic": "Tráfico",
"in": "In", "in": "Entrante",
"out": "Out" "out": "Saliente"
}, },
"peanut": { "peanut": {
"battery_charge": "Carga de la batería", "battery_charge": "Carga de la batería",
@@ -769,7 +770,7 @@
"gross_percent_today": "Hoy", "gross_percent_today": "Hoy",
"gross_percent_1y": "Un año", "gross_percent_1y": "Un año",
"gross_percent_max": "Todo el tiempo", "gross_percent_max": "Todo el tiempo",
"net_worth": "Net Worth" "net_worth": "Patrimonio neto"
}, },
"audiobookshelf": { "audiobookshelf": {
"podcasts": "Podcasts", "podcasts": "Podcasts",

View File

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

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "En ligne", "up": "En ligne",
"pending": "En attente", "pending": "En attente",
"down": "Hors ligne" "down": "Hors ligne",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Nouveau", "new": "Nouveau",

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "למעלה", "up": "למעלה",
"pending": "ממתין", "pending": "ממתין",
"down": "למטה" "down": "למטה",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "חדש", "new": "חדש",

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Aktivno", "up": "Aktivno",
"pending": "U tijeku", "pending": "U tijeku",
"down": "Neaktivno" "down": "Neaktivno",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Novo", "new": "Novo",

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Fut", "up": "Fut",
"pending": "Függőben lévő", "pending": "Függőben lévő",
"down": "Leállt" "down": "Leállt",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Új", "new": "Új",

View File

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

View File

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

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "업", "up": "업",
"pending": "대기 중", "pending": "대기 중",
"down": "다운" "down": "다운",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "신규", "new": "신규",

View File

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

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Online", "up": "Online",
"pending": "In afwachting", "pending": "In afwachting",
"down": "Offline" "down": "Offline",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Nieuw", "new": "Nieuw",

View File

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

View File

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

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Ativo", "up": "Ativo",
"pending": "Pendente", "pending": "Pendente",
"down": "Inativo" "down": "Inativo",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Novo", "new": "Novo",

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "В сети", "up": "В сети",
"pending": "Ожидают", "pending": "Ожидают",
"down": "Не в сети" "down": "Не в сети",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Новый", "new": "Новый",

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Beží", "up": "Beží",
"pending": "Čakajúce", "pending": "Čakajúce",
"down": "Down" "down": "Down",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Nový", "new": "Nový",

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Горе", "up": "Горе",
"pending": "На чекању", "pending": "На чекању",
"down": "Доле" "down": "Доле",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Сада", "new": "Сада",
@@ -600,13 +601,13 @@
"total": "Укупно" "total": "Укупно"
}, },
"pangolin": { "pangolin": {
"orgs": "Orgs", "orgs": "Организације",
"sites": "Sites", "sites": "Сајтови",
"resources": "Resources", "resources": "Ресурси",
"targets": "Targets", "targets": "Циљеви",
"traffic": "Traffic", "traffic": "Саобраћај",
"in": "In", "in": "Улазак",
"out": "Out" "out": "Излазак"
}, },
"peanut": { "peanut": {
"battery_charge": "Напуњеност батерије", "battery_charge": "Напуњеност батерије",

View File

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

View File

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

View File

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

View File

@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Çalışıyor", "up": "Çalışıyor",
"pending": "Bekleyen", "pending": "Bekleyen",
"down": "Çalışmayan" "down": "Çalışmayan",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "Yeni", "new": "Yeni",

View File

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

View File

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

View File

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

View File

@@ -364,7 +364,7 @@
"version": "版本", "version": "版本",
"notesCount": "笔记", "notesCount": "笔记",
"dbSize": "数据库大小", "dbSize": "数据库大小",
"unknown": "Unknown" "unknown": "未知"
}, },
"navidrome": { "navidrome": {
"nothing_streaming": "", "nothing_streaming": "",
@@ -532,7 +532,8 @@
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Up", "up": "Up",
"pending": "Pending", "pending": "Pending",
"down": "Down" "down": "Down",
"ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "新建立", "new": "新建立",
@@ -800,7 +801,7 @@
}, },
"kavita": { "kavita": {
"seriesCount": "系列", "seriesCount": "系列",
"totalFiles": "Files" "totalFiles": "文件"
}, },
"azuredevops": { "azuredevops": {
"result": "Result", "result": "Result",
@@ -1097,7 +1098,7 @@
}, },
"unraid": { "unraid": {
"STARTED": "Started", "STARTED": "Started",
"STOPPED": "Stopped", "STOPPED": "已停止",
"NEW_ARRAY": "New Array", "NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk", "RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled", "DISABLE_DISK": "Disk Disabled",

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,18 @@ body,
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 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, html,

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,15 @@ const logger = createLogger(proxyName);
const sessionCacheKey = `${proxyName}__sessionId`; const sessionCacheKey = `${proxyName}__sessionId`;
const isNgCacheKey = `${proxyName}__isNg`; 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) { async function fetchFromPyloadAPI(url, sessionId, params, service) {
const options = { const options = {
body: params body: params
@@ -33,13 +42,33 @@ async function fetchFromPyloadAPI(url, sessionId, params, service) {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const [status, contentType, data, responseHeaders] = await httpProxy(url, options); const [status, contentType, data, responseHeaders] = await httpProxy(url, options);
let returnData; const returnData = parsePyloadResponse(url, data);
try { return [status, returnData, responseHeaders];
returnData = JSON.parse(Buffer.from(data).toString()); }
} catch (e) {
logger.error(`Error communicating with pyload API at ${url}, returned: ${JSON.stringify(data)}`); async function fetchFromPyloadAPIBasic(url, params, username, password) {
returnData = data; 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);
} }
// 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]; return [status, returnData, responseHeaders];
} }
@@ -66,24 +95,43 @@ async function login(loginUrl, service, username, password = "") {
return sessionId; return sessionId;
} }
export default async function pyloadProxyHandler(req, res) { export default async function pyloadProxyHandler(req, res, map = {}) {
const { group, service, endpoint, index } = req.query; const { group, service, endpoint, index } = req.query;
const { ngEndpoint } = map;
try { try {
if (group && service) { if (group && service) {
const widget = await getServiceWidget(group, service, index); const widget = await getServiceWidget(group, service, index);
if (widget) { if (widget) {
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...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 loginUrl = `${widget.url}/api/login`; 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 = let sessionId =
cache.get(`${sessionCacheKey}.${service}`) ?? cache.get(`${sessionCacheKey}.${service}`) ??
(await login(loginUrl, service, widget.username, widget.password)); (await login(loginUrl, service, widget.username, widget.password));
let [status, data] = await fetchFromPyloadAPI(url, sessionId, null, service); let [status, data] = await fetchFromPyloadAPI(url, sessionId, null, service);
if (status === 403 || status === 401) { if (status === 403 || status === 401 || (status === 400 && data?.error?.includes("CSRF token"))) {
logger.info("Failed to retrieve data from Pyload API, trying to login again..."); logger.info("Failed to retrieve data from Pyload API with session auth, trying to login again...");
cache.del(`${sessionCacheKey}.${service}`); cache.del(`${sessionCacheKey}.${service}`);
sessionId = await login(loginUrl, service, widget.username, widget.password); sessionId = await login(loginUrl, service, widget.username, widget.password);
[status, data] = await fetchFromPyloadAPI(url, sessionId, null, service); [status, data] = await fetchFromPyloadAPI(url, sessionId, null, service);

View File

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

View File

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