Compare commits

..

23 Commits

Author SHA1 Message Date
shamoon
7d1e0c087a Bump version to 1.6.1 2025-11-04 16:20:12 -08:00
shamoon
d48ef4c038 Merge branch 'dev' 2025-11-04 16:19:56 -08:00
shamoon
4a2eeaa8b9 Fix: correct cached version check (#5933) 2025-11-04 13:59:35 -08:00
shamoon
faa2e6bb36 Fix: ensure minimum height for inner container (#5930) 2025-11-04 10:12:49 -08:00
shamoon
438543d8cd Bump version to 1.6.0 2025-11-04 08:07:34 -08:00
shamoon
5a350cc9ce Merge branch 'dev' 2025-11-04 08:07:14 -08:00
github-actions[bot]
529814cf03 New Crowdin translations by GitHub Action (#5802)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-11-04 07:54:08 -08:00
shamoon
9b5275a854 Enhancement: support omada controller v6 (#5926) 2025-11-04 06:17:00 -08:00
dependabot[bot]
e623196ac0 Chore(deps): Bump pretty-bytes from 6.1.1 to 7.1.0 (#5917)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 00:31:10 +00:00
dependabot[bot]
973b1f7aaf Chore(deps): Bump @headlessui/react from 2.2.7 to 2.2.9 (#5919)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 00:18:52 +00:00
dependabot[bot]
81a322cc99 Chore(deps-dev): Bump eslint-config-prettier from 10.1.1 to 10.1.8 (#5918)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 00:02:46 +00:00
dependabot[bot]
36e82a8b90 Chore(deps-dev): Bump prettier-plugin-organize-imports from 4.1.0 to 4.3.0 (#5915)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 23:53:02 +00:00
shamoon
1383e22acd Change: use glances memory available instead of free (#5923) 2025-11-03 15:32:36 -08:00
Charles Ng
a756a01d63 Documentation: correct Unraid widget allowed fields (#5908) 2025-10-29 18:52:15 -07:00
oharvey2090
937efc9f1b Performance: emby widget prevent sessions query if now playing disabled (#5907) 2025-10-29 18:50:08 -07:00
shamoon
fe6f32f072 Update getting-started.md 2025-10-29 14:29:17 -07:00
shamoon
226603770c Rename docker stats to container 2025-10-23 14:33:20 -07:00
Darkangeel_hd
2f48d21bfd Change: adjust MySpeed blocks order (#5881) 2025-10-16 18:42:33 -07:00
dependabot[bot]
4457baffa5 Chore(deps): Bump actions/setup-node from 5 to 6 (#5873)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 09:15:40 -07:00
shamoon
91d12c401c Feature: fields highlighting (#5868) 2025-10-13 19:37:52 -07:00
shamoon
3f8da51aeb Fix: fix uptime robot for empty logs (#5866) 2025-10-13 07:43:26 -07:00
shamoon
837717461f Fix: count only error status as failures in backrest (#5844) 2025-10-04 07:37:22 -07:00
shamoon
f7a6b7dbf4 Documentation: clarify Unraid widget API key requirement, for now 2025-09-23 08:48:53 -07:00
27 changed files with 777 additions and 389 deletions

View File

@@ -38,7 +38,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 20
cache: 'pnpm'
@@ -96,7 +96,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 20
cache: 'pnpm'

View File

@@ -271,4 +271,4 @@ You can show the docker stats by clicking the status indicator but this can also
showStats: true
```
Also see the settings for [show docker stats](settings.md#show-docker-stats).
Also see the settings for [show docker stats](settings.md#show-container-stats).

View File

@@ -118,6 +118,47 @@ Each widget can optionally provide a list of which fields should be visible via
key: apikeyapikeyapikeyapikeyapikey
```
### Block Highlighting
Widgets can tint their metric block text automatically based on rules defined alongside the service. Attach a `highlight` section to the widget configuration and map each block to one or more numeric or string rules using the field key (for example, `queued`, `lan_users`).
```yaml
- Sonarr:
icon: sonarr.png
href: http://sonarr.host.or.ip
widget:
type: sonarr
url: http://sonarr.host.or.ip
key: ${SONARR_API_KEY}
highlight:
queued:
numeric:
- level: danger
when: gte
value: 20
- level: warn
when: gte
value: 5
- level: good
when: eq
value: 0
status:
string:
- level: danger
when: regex
value: "(failed|import) pending"
- level: good
when: equals
value: "All good"
status_code:
string:
- level: warn
when: regex
value: "^5\\d{2}$"
```
Supported numeric operators for the `when` property are `gt`, `gte`, `lt`, `lte`, `eq`, `ne`, `between`, and `outside`. String rules support `equals`, `includes`, `startsWith`, `endsWith`, and `regex`. Each rule can be inverted with `negate: true`, and string rules may pass `caseSensitive: true` or custom regex `flags`. The highlight engine does its best to coerce formatted values, but you will get the most reliable results when you pass plain numbers or strings into `<Block>`.
## Descriptions
Services may have descriptions,

View File

@@ -109,6 +109,20 @@ color: slate
Supported colors are: `slate`, `gray`, `zinc`, `neutral`, `stone`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, `red`, `white`
## Block Highlight Levels
You can override the default Tailwind classes applied when a widget highlight rule resolves to the `good`, `warn`, or `danger` level.
```yaml
blockHighlights:
levels:
good: "bg-emerald-500/40 text-emerald-950 dark:bg-emerald-900/60 dark:text-emerald-400"
warn: "bg-amber-300/30 text-amber-900 dark:bg-amber-900/30 dark:text-amber-200"
danger: "bg-rose-700/45 text-rose-200 dark:bg-rose-950/70 dark:text-rose-400"
```
Any unspecified level falls back to the built-in defaults.
## Layout
You can configure service and bookmarks sections to be either "column" or "row" based layouts, like so:
@@ -486,9 +500,9 @@ logpath: /logfile/path
By default, logs are sent both to `stdout` and to a file at the path specified. This can be changed by setting the `LOG_TARGETS` environment variable to one of `both` (default), `stdout` or `file`.
## Show Docker Stats
## Show Container Stats
You can show all docker stats expanded in `settings.yaml`:
You can show all docker or proxmox stats expanded in `settings.yaml`:
```yaml
showStats: true

View File

@@ -62,3 +62,4 @@ To ensure cohesiveness of various widgets, the following should be used as a gui
- Minimize the number of API calls
- Avoid the use of custom proxy unless absolutely necessary
- Widgets should be 'read-only', as in they should not make write changes using the relevant tool's API. Homepage widgets are designed to surface information, not to be a (usually worse) replacement for the tool itself.
- Widgets should not allow manually overriding the "refresh interval" setting, as misconfigured refresh intervals can easily lead to performance issues for users.

View File

@@ -10,11 +10,11 @@ The Unraid widget allows you to monitor the resources of an Unraid server.
**Minimum Requirements:**
- Unraid 7.2 -or- Unraid Connect plugin 2025.08.19.1850
- API key with the **GUEST** (read only) role: [Managing API Keys](https://docs.unraid.net/go/managing-api-keys)
- API key with the **ADMIN** role: [Managing API Keys](https://docs.unraid.net/go/managing-api-keys)
The widget can display metrics for selected Unraid pools. If using one of the "pool" fields, you must also add the pool name to the settings.
**Allowed fields:** `["cpu","memoryPercent","memoryAvailable","memoryUsed","notifications","arrayFreeSpace","arrayUsedSpace","arrayUsedPercent","status","pool1UsedSpace","pool1FreeSpace","pool1UsedPercent","pool2UsedSpace","pool2FreeSpace","pool2UsedPercent","pool3UsedSpace","pool3FreeSpace","pool3UsedPercent","pool4UsedSpace","pool4FreeSpace","pool4UsedPercent"]`
**Allowed fields:** `["cpu","memoryPercent","memoryAvailable","memoryUsed","notifications","arrayFree","arrayUsedSpace","arrayUsedPercent","status","pool1UsedSpace","pool1FreeSpace","pool1UsedPercent","pool2UsedSpace","pool2FreeSpace","pool2UsedPercent","pool3UsedSpace","pool3FreeSpace","pool3UsedPercent","pool4UsedSpace","pool4FreeSpace","pool4UsedPercent"]`
```yaml
widget:

View File

@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.5.0",
"version": "1.6.1",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -11,7 +11,7 @@
"telemetry": "next telemetry disable"
},
"dependencies": {
"@headlessui/react": "^2.2.7",
"@headlessui/react": "^2.2.9",
"@kubernetes/client-node": "^1.0.0",
"classnames": "^2.5.1",
"compare-versions": "^6.1.1",
@@ -28,7 +28,7 @@
"next": "^15.5.2",
"next-i18next": "^12.1.0",
"ping": "^0.4.4",
"pretty-bytes": "^6.1.1",
"pretty-bytes": "^7.1.0",
"raw-body": "^3.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -47,7 +47,7 @@
"@tailwindcss/postcss": "^4.1.14",
"eslint": "^9.25.1",
"eslint-config-next": "^15.2.4",
"eslint-config-prettier": "^10.1.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.4",
@@ -55,7 +55,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-organize-imports": "^4.3.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.0.9",
"typescript": "^5.7.3"

111
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@headlessui/react':
specifier: ^2.2.7
version: 2.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^2.2.9
version: 2.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@kubernetes/client-node':
specifier: ^1.0.0
version: 1.0.0
@@ -60,8 +60,8 @@ importers:
specifier: ^0.4.4
version: 0.4.4
pretty-bytes:
specifier: ^6.1.1
version: 6.1.1
specifier: ^7.1.0
version: 7.1.0
raw-body:
specifier: ^3.0.1
version: 3.0.1
@@ -112,8 +112,8 @@ importers:
specifier: ^15.2.4
version: 15.2.4(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3)
eslint-config-prettier:
specifier: ^10.1.1
version: 10.1.1(eslint@9.25.1(jiti@2.6.1))
specifier: ^10.1.8
version: 10.1.8(eslint@9.25.1(jiti@2.6.1))
eslint-plugin-import:
specifier: ^2.32.0
version: 2.32.0(@typescript-eslint/parser@8.29.0(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.1(jiti@2.6.1))
@@ -122,7 +122,7 @@ importers:
version: 6.10.2(eslint@9.25.1(jiti@2.6.1))
eslint-plugin-prettier:
specifier: ^5.5.4
version: 5.5.4(eslint-config-prettier@10.1.1(eslint@9.25.1(jiti@2.6.1)))(eslint@9.25.1(jiti@2.6.1))(prettier@3.6.2)
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.25.1(jiti@2.6.1)))(eslint@9.25.1(jiti@2.6.1))(prettier@3.6.2)
eslint-plugin-react:
specifier: ^7.37.4
version: 7.37.4(eslint@9.25.1(jiti@2.6.1))
@@ -136,8 +136,8 @@ importers:
specifier: ^3.6.2
version: 3.6.2
prettier-plugin-organize-imports:
specifier: ^4.1.0
version: 4.1.0(prettier@3.6.2)(typescript@5.7.3)
specifier: ^4.3.0
version: 4.3.0(prettier@3.6.2)(typescript@5.7.3)
tailwind-scrollbar:
specifier: ^4.0.2
version: 4.0.2(react@18.3.1)(tailwindcss@4.0.9)
@@ -257,8 +257,8 @@ packages:
engines: {node: '>=6'}
hasBin: true
'@headlessui/react@2.2.7':
resolution: {integrity: sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==}
'@headlessui/react@2.2.9':
resolution: {integrity: sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
@@ -559,14 +559,14 @@ packages:
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@react-aria/focus@3.21.1':
resolution: {integrity: sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==}
'@react-aria/focus@3.21.2':
resolution: {integrity: sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-aria/interactions@3.25.5':
resolution: {integrity: sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==}
'@react-aria/interactions@3.25.6':
resolution: {integrity: sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@@ -577,8 +577,8 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-aria/utils@3.30.1':
resolution: {integrity: sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==}
'@react-aria/utils@3.31.0':
resolution: {integrity: sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@@ -591,8 +591,8 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-types/shared@3.32.0':
resolution: {integrity: sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==}
'@react-types/shared@3.32.1':
resolution: {integrity: sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@@ -1411,8 +1411,8 @@ packages:
typescript:
optional: true
eslint-config-prettier@10.1.1:
resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==}
eslint-config-prettier@10.1.8:
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
@@ -2361,12 +2361,12 @@ packages:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'}
prettier-plugin-organize-imports@4.1.0:
resolution: {integrity: sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==}
prettier-plugin-organize-imports@4.3.0:
resolution: {integrity: sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==}
peerDependencies:
prettier: '>=2.0'
typescript: '>=2.9'
vue-tsc: ^2.1.0
vue-tsc: ^2.1.0 || 3
peerDependenciesMeta:
vue-tsc:
optional: true
@@ -2376,9 +2376,9 @@ packages:
engines: {node: '>=14'}
hasBin: true
pretty-bytes@6.1.1:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
pretty-bytes@7.1.0:
resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
engines: {node: '>=20'}
prism-react-renderer@2.4.1:
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
@@ -2782,8 +2782,8 @@ packages:
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tabbable@6.3.0:
resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==}
tailwind-scrollbar@4.0.2:
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
@@ -2930,6 +2930,11 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -3148,7 +3153,7 @@ snapshots:
'@floating-ui/utils': 0.2.10
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tabbable: 6.2.0
tabbable: 6.3.0
'@floating-ui/utils@0.2.10': {}
@@ -3164,15 +3169,15 @@ snapshots:
protobufjs: 7.5.3
yargs: 17.7.2
'@headlessui/react@2.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@headlessui/react@2.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/focus': 3.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/interactions': 3.25.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/focus': 3.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/interactions': 3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.5.0(react@18.3.1)
use-sync-external-store: 1.6.0(react@18.3.1)
'@humanfs/core@0.19.1': {}
@@ -3419,22 +3424,22 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
'@react-aria/focus@3.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@react-aria/focus@3.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@react-aria/interactions': 3.25.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/utils': 3.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-types/shared': 3.32.0(react@18.3.1)
'@react-aria/interactions': 3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-types/shared': 3.32.1(react@18.3.1)
'@swc/helpers': 0.5.17
clsx: 2.1.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@react-aria/interactions@3.25.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@react-aria/interactions@3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@react-aria/ssr': 3.9.10(react@18.3.1)
'@react-aria/utils': 3.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-stately/flags': 3.1.2
'@react-types/shared': 3.32.0(react@18.3.1)
'@react-types/shared': 3.32.1(react@18.3.1)
'@swc/helpers': 0.5.17
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -3444,12 +3449,12 @@ snapshots:
'@swc/helpers': 0.5.17
react: 18.3.1
'@react-aria/utils@3.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@react-aria/utils@3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@react-aria/ssr': 3.9.10(react@18.3.1)
'@react-stately/flags': 3.1.2
'@react-stately/utils': 3.10.8(react@18.3.1)
'@react-types/shared': 3.32.0(react@18.3.1)
'@react-types/shared': 3.32.1(react@18.3.1)
'@swc/helpers': 0.5.17
clsx: 2.1.1
react: 18.3.1
@@ -3464,7 +3469,7 @@ snapshots:
'@swc/helpers': 0.5.17
react: 18.3.1
'@react-types/shared@3.32.0(react@18.3.1)':
'@react-types/shared@3.32.1(react@18.3.1)':
dependencies:
react: 18.3.1
@@ -4415,7 +4420,7 @@ snapshots:
- eslint-plugin-import-x
- supports-color
eslint-config-prettier@10.1.1(eslint@9.25.1(jiti@2.6.1)):
eslint-config-prettier@10.1.8(eslint@9.25.1(jiti@2.6.1)):
dependencies:
eslint: 9.25.1(jiti@2.6.1)
@@ -4501,14 +4506,14 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.1(eslint@9.25.1(jiti@2.6.1)))(eslint@9.25.1(jiti@2.6.1))(prettier@3.6.2):
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.25.1(jiti@2.6.1)))(eslint@9.25.1(jiti@2.6.1))(prettier@3.6.2):
dependencies:
eslint: 9.25.1(jiti@2.6.1)
prettier: 3.6.2
prettier-linter-helpers: 1.0.0
synckit: 0.11.11
optionalDependencies:
eslint-config-prettier: 10.1.1(eslint@9.25.1(jiti@2.6.1))
eslint-config-prettier: 10.1.8(eslint@9.25.1(jiti@2.6.1))
eslint-plugin-react-hooks@5.2.0(eslint@9.25.1(jiti@2.6.1)):
dependencies:
@@ -5411,14 +5416,14 @@ snapshots:
dependencies:
fast-diff: 1.3.0
prettier-plugin-organize-imports@4.1.0(prettier@3.6.2)(typescript@5.7.3):
prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.7.3):
dependencies:
prettier: 3.6.2
typescript: 5.7.3
prettier@3.6.2: {}
pretty-bytes@6.1.1: {}
pretty-bytes@7.1.0: {}
prism-react-renderer@2.4.1(react@18.3.1):
dependencies:
@@ -5504,7 +5509,7 @@ snapshots:
dependencies:
'@types/use-sync-external-store': 0.0.6
react: 18.3.1
use-sync-external-store: 1.5.0(react@18.3.1)
use-sync-external-store: 1.6.0(react@18.3.1)
optionalDependencies:
'@types/react': 19.0.10
redux: 5.0.1
@@ -5902,7 +5907,7 @@ snapshots:
systeminformation@5.27.7: {}
tabbable@6.2.0: {}
tabbable@6.3.0: {}
tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@4.0.9):
dependencies:
@@ -6089,6 +6094,10 @@ snapshots:
dependencies:
react: 18.3.1
use-sync-external-store@1.6.0(react@18.3.1):
dependencies:
react: 18.3.1
util-deprecate@1.0.2: {}
uuid@10.0.0: {}

View File

@@ -1093,7 +1093,7 @@
"DISABLE_DISK": "Skyf Gedeaktiveer",
"SWAP_DSBL": "Ruil Gedeaktiveer",
"INVALID_EXPANSION": "Ongeldige Uitbreiding",
"PARITY_NOT_BIGGEST": "Pariteit nie die grootste nie",
"PARITY_NOT_BIGGEST": "Pariteit nie die Grootste nie",
"TOO_MANY_MISSING_DISKS": "Te Veel Ontbrekende Skywe",
"NEW_DISK_TOO_SMALL": "Nuwe Skyf te Klein",
"NO_DATA_DISKS": "Geen Data Skywe",

View File

@@ -93,8 +93,8 @@
"http_status": "HTTP-Status",
"error": "Fehler",
"response": "Antwort",
"down": "Online",
"up": "Offline",
"down": "Offline",
"up": "Online",
"not_available": "Nicht verfügbar"
},
"emby": {
@@ -276,7 +276,7 @@
"pending": "Wartend",
"approved": "Genehmigt",
"available": "Verfügbar",
"issues": "Open Issues"
"issues": "Offene Issues"
},
"overseerr": {
"pending": "Wartend",
@@ -1088,22 +1088,22 @@
"unraid": {
"STARTED": "Gestartet",
"STOPPED": "Angehalten",
"NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",
"SWAP_DSBL": "Swap Disable",
"INVALID_EXPANSION": "Invalid Expansion",
"NEW_ARRAY": "Neues Array",
"RECON_DISK": "Festplatte wird neu aufgebaut",
"DISABLE_DISK": "Festplatte deaktiviert",
"SWAP_DSBL": "Swap deaktivieren",
"INVALID_EXPANSION": "Üngültige Erweiterung",
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
"TOO_MANY_MISSING_DISKS": "Zu viele fehlende Festplatten",
"NEW_DISK_TOO_SMALL": "Neue Festplatte zu klein",
"NO_DATA_DISKS": "Keine Datenträger",
"notifications": "Mitteilungen",
"status": "Status",
"cpu": "CPU",
"memoryUsed": "Speichernutzung",
"memoryAvailable": "Verfügbarer Speicher",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"arrayUsed": "Array verwendet",
"arrayFree": "Array frei",
"poolUsed": "{{pool}} verwendet",
"poolFree": "{{pool}} frei"
},
@@ -1111,9 +1111,9 @@
"num_plans": "Pläne",
"num_success_30": "Erfolgreich",
"num_failure_30": "Fehlerhaft",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
"num_success_latest": "Erfolgreich",
"num_failure_latest": "Fehlgeschlagen",
"bytes_added_30": "Bytes hinzugefügt"
},
"yourspotify": {
"songs": "Titel",

View File

@@ -276,7 +276,7 @@
"pending": "Pendiente",
"approved": "Aprobado",
"available": "Disponible",
"issues": "Open Issues"
"issues": "Issues Abiertos"
},
"overseerr": {
"pending": "Pendiente",
@@ -1108,16 +1108,16 @@
"poolFree": "{{pool}} Libre"
},
"backrest": {
"num_plans": "Plans",
"num_success_30": "Successes",
"num_failure_30": "Failures",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
"num_plans": "Planes",
"num_success_30": "Éxitos",
"num_failure_30": "Fallos",
"num_success_latest": "Exitosa",
"num_failure_latest": "Fallida",
"bytes_added_30": "Bytes Añadidos"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
"songs": "Canciones",
"time": "Tiempo",
"artists": "Artistas"
}
}

View File

@@ -276,7 +276,7 @@
"pending": "En attente",
"approved": "Approuvé",
"available": "Disponible",
"issues": "Open Issues"
"issues": "Problèmes non résolus"
},
"overseerr": {
"pending": "En attente",
@@ -1086,38 +1086,38 @@
"nextRenewingSubscription": "Prochain paiement"
},
"unraid": {
"STARTED": "Started",
"STOPPED": "Stopped",
"NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",
"STARTED": "Commencé",
"STOPPED": "Arrêté",
"NEW_ARRAY": "Nouveau tableau",
"RECON_DISK": "Reconstruction du disque",
"DISABLE_DISK": "Disque désactivé",
"SWAP_DSBL": "Swap Disable",
"INVALID_EXPANSION": "Invalid Expansion",
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
"NO_DATA_DISKS": "No Data Disks",
"INVALID_EXPANSION": "Extension invalide",
"PARITY_NOT_BIGGEST": "La parité n'est pas la plus grande",
"TOO_MANY_MISSING_DISKS": "Trop de disques manquants",
"NEW_DISK_TOO_SMALL": "Nouveau disque trop petit",
"NO_DATA_DISKS": "Aucun disque de données",
"notifications": "Notifications",
"status": "État",
"cpu": "UCT",
"memoryUsed": "Mémoire Utilisé",
"memoryAvailable": "Mémoire Disponible",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"poolUsed": "{{pool}} Used",
"poolFree": "{{pool}} Free"
"arrayUsed": "RAID utilisé",
"arrayFree": "RAID libre",
"poolUsed": "{{pool}} Utilisé",
"poolFree": "{{pool}} Libre"
},
"backrest": {
"num_plans": "Plans",
"num_success_30": "Successes",
"num_failure_30": "Failures",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
"num_plans": "Abonnements",
"num_success_30": "Succès",
"num_failure_30": "Échecs",
"num_success_latest": "Réussi",
"num_failure_latest": "Échoué",
"bytes_added_30": "Octets ajoutés"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
"songs": "Musiques",
"time": "Durée",
"artists": "Artistes"
}
}

View File

@@ -61,7 +61,7 @@
"wlan_devices": "Dispositivos WLAN",
"lan_users": "Usuários de LAN",
"wlan_users": "Usuários de WLAN",
"up": "UP",
"up": "ATIVO",
"down": "Desligado",
"wait": "Por favor, aguarde",
"empty_data": "Status do Subsistema desconhecido"
@@ -246,7 +246,7 @@
"unknown": "Desconhecido"
},
"radarr": {
"wanted": "Wanted",
"wanted": "Desejado",
"missing": "Faltando",
"queued": "Em fila",
"movies": "Filmes",
@@ -254,13 +254,13 @@
"unknown": "Desconhecido"
},
"lidarr": {
"wanted": "Wanted",
"queued": "Queued",
"wanted": "Desejado",
"queued": "Na fila",
"artists": "Artistas"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"wanted": "Desejado",
"queued": "Na fila",
"books": "Livros"
},
"bazarr": {
@@ -273,20 +273,20 @@
"available": "Disponível"
},
"jellyseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"issues": "Open Issues"
"pending": "Pendente",
"approved": "Aprovado",
"available": "Disponível",
"issues": "Incidentes Abertos"
},
"overseerr": {
"pending": "Pending",
"pending": "Pendente",
"processing": "Processando",
"approved": "Approved",
"available": "Available"
"approved": "Aprovado",
"available": "Disponível"
},
"netalertx": {
"total": "Total",
"connected": "Connected",
"connected": "Conectado",
"new_devices": "Novos dispositivos",
"down_alerts": "Alertas de Inatividade"
},
@@ -297,26 +297,26 @@
"gravity": "Gravidade"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"queries": "Consultas",
"blocked": "Bloqueado",
"filtered": "Filtrado",
"latency": "Latência"
},
"speedtest": {
"upload": "Upload",
"download": "Download",
"upload": "Enviar",
"download": "Baixar",
"ping": "Ping"
},
"portainer": {
"running": "Running",
"running": "Executando",
"stopped": "Parado",
"total": "Total"
},
"suwayomi": {
"download": "Downloaded",
"download": "Baixado",
"nondownload": "Não Baixado",
"read": "Read",
"unread": "Unread",
"read": "Lido",
"unread": "Não lido",
"downloadedread": "Baixado e Lido",
"downloadedunread": "Baixado e Não Lido",
"nondownloadedread": "Não Baixado e Lido",
@@ -337,7 +337,7 @@
"ago": "{{value}} Atrás"
},
"technitium": {
"totalQueries": "Queries",
"totalQueries": "Consultas",
"totalNoError": "Sucesso",
"totalServerFailure": "Falhas",
"totalNxDomain": "Domínios NX",
@@ -345,12 +345,12 @@
"totalAuthoritative": "Autoritativo",
"totalRecursive": "Recursivo",
"totalCached": "Em cache",
"totalBlocked": "Blocked",
"totalBlocked": "Bloqueado",
"totalDropped": "Perdidos",
"totalClients": "Clientes"
},
"tdarr": {
"queue": "Queue",
"queue": "Fila de espera",
"processed": "Processado",
"errored": "Erro",
"saved": "Guardado"
@@ -361,13 +361,13 @@
"middleware": ""
},
"trilium": {
"version": "Version",
"version": "Versão",
"notesCount": "Notas",
"dbSize": "Tamanho do banco de dados",
"unknown": "Unknown"
"unknown": "Desconhecido"
},
"navidrome": {
"nothing_streaming": "No Active Streams",
"nothing_streaming": "Sem Streams Ativos",
"please_wait": "Por favor, aguarde"
},
"npm": {
@@ -384,49 +384,49 @@
},
"gotify": {
"apps": "Aplicações",
"clients": "Clients",
"clients": "Clientes",
"messages": "Mensagens"
},
"prowlarr": {
"enableIndexers": "Indexadores",
"numberOfGrabs": "Agarrados",
"numberOfQueries": "Queries",
"numberOfQueries": "Consultas",
"numberOfFailGrabs": "Falhados",
"numberOfFailQueries": "Pesquisas falhadas"
},
"jackett": {
"configured": "Configurado",
"errored": "Errored"
"errored": "Falhou"
},
"strelaysrv": {
"numActiveSessions": "Sessões",
"numConnections": "Conexões",
"dataRelayed": "Retransmitido",
"transferRate": "Rate"
"transferRate": "Taxa"
},
"mastodon": {
"user_count": "Users",
"user_count": "Usuários",
"status_count": "Postagens",
"domain_count": "Domínios"
},
"medusa": {
"wanted": "Wanted",
"queued": "Queued",
"series": "Series"
"wanted": "Desejado",
"queued": "Na fila",
"series": "Séries"
},
"minecraft": {
"players": "Reprodutores",
"version": "Versão",
"status": "Status",
"up": "Online",
"down": "Offline"
"down": "Desconectado"
},
"miniflux": {
"read": "Lido",
"unread": "Unread"
"unread": "Não lido"
},
"authentik": {
"users": "Users",
"users": "Usuários",
"loginsLast24H": "Inícios de sessão (24h)",
"failedLoginsLast24H": "Inícios de sessão falhados (24h)"
},
@@ -438,19 +438,19 @@
},
"glances": {
"cpu": "CPU",
"load": "Load",
"wait": "Please wait",
"temp": "TEMP",
"load": "Carga",
"wait": "Por favor, aguarde",
"temp": "TEMPERATURA",
"_temp": "Temperatura",
"warn": "Aviso",
"uptime": "UP",
"uptime": "ATIVO",
"total": "Total",
"free": "Free",
"used": "Used",
"free": "Livre",
"used": "Utilizado",
"days": "d",
"hours": "h",
"crit": "Crítico",
"read": "Read",
"read": "Leitura",
"write": "Escrita",
"gpu": "GPU",
"mem": "Memória",
@@ -471,57 +471,57 @@
"1-day": "Maioritariamente ensolarado",
"1-night": "Maioritariamente Limpo",
"2-day": "Parcialmente Nublado",
"2-night": "Partly Cloudy",
"2-night": "Parcialmente Nublado",
"3-day": "Nublado",
"3-night": "Cloudy",
"3-night": "Nublado",
"45-day": "Nevoeiro",
"45-night": "Foggy",
"48-day": "Foggy",
"48-night": "Foggy",
"45-night": "Nevoeiro",
"48-day": "Nevoeiro",
"48-night": "Nevoeiro",
"51-day": "Aguaceiros",
"51-night": "Light Drizzle",
"51-night": "Leve Garoa",
"53-day": "Chuvisco",
"53-night": "Drizzle",
"53-night": "Garoa",
"55-day": "Aguaceiro Forte",
"55-night": "Heavy Drizzle",
"55-night": "Garoa Forte",
"56-day": "Leve Garoa Congelante",
"56-night": "Light Freezing Drizzle",
"56-night": "Garoa Congelante Fraca",
"57-day": "Garoa Congelante",
"57-night": "Freezing Drizzle",
"57-night": "Garoa Congelante",
"61-day": "Chuva fraca",
"61-night": "Light Rain",
"61-night": "Chuva Fraca",
"63-day": "Chuva",
"63-night": "Rain",
"63-night": "Chuva",
"65-day": "Chuva forte",
"65-night": "Heavy Rain",
"65-night": "Chuva Forte",
"66-day": "Chuva Congelante",
"66-night": "Freezing Rain",
"67-day": "Freezing Rain",
"67-night": "Freezing Rain",
"66-night": "Chuva Congelante",
"67-day": "Chuva Congelante",
"67-night": "Chuva Congelante",
"71-day": "Neve fraca",
"71-night": "Light Snow",
"71-night": "Neve Fraca",
"73-day": "Neve",
"73-night": "Snow",
"73-night": "Neve",
"75-day": "Neve forte",
"75-night": "Heavy Snow",
"75-night": "Neve Forte",
"77-day": "Grãos de Neve",
"77-night": "Snow Grains",
"77-night": "Grãos de Neve",
"80-day": "Neve fraca",
"80-night": "Light Showers",
"80-night": "Pancadas de Chuva Leves",
"81-day": "Chuviscos",
"81-night": "Showers",
"81-night": "Pancadas de Chuva",
"82-day": "Chuviscos fortes",
"82-night": "Heavy Showers",
"82-night": "Pancadas de Chuva Forte",
"85-day": "Precipitação de Neve",
"85-night": "Snow Showers",
"86-day": "Snow Showers",
"86-night": "Snow Showers",
"85-night": "Pancadas de Neve",
"86-day": "Pancadas de Neve",
"86-night": "Pancadas de Neve",
"95-day": "Trovoada",
"95-night": "Thunderstorm",
"95-night": "Tempestade Com Raios",
"96-day": "Trovoada com granizo",
"96-night": "Thunderstorm With Hail",
"99-day": "Thunderstorm With Hail",
"99-night": "Thunderstorm With Hail"
"96-night": "Tempestade Com Raios e Granizo",
"99-day": "Tempestade Com Raios e Granizo",
"99-night": "Tempestade Com Raios e Granizo"
},
"homebridge": {
"available_update": "Sistema",
@@ -530,15 +530,15 @@
"up_to_date": "Atualizado",
"child_bridges": "Pontes Filhas",
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down"
"up": "Ativo",
"pending": "Pendente",
"down": "Inativo"
},
"healthchecks": {
"new": "Novo",
"up": "Up",
"up": "Ativo",
"grace": "Em Período Gratuito",
"down": "Down",
"down": "Inativo",
"paused": "Pausado",
"status": "Status",
"last_ping": "Ultimo Ping",
@@ -550,26 +550,26 @@
"containers_failed": "Falhou"
},
"autobrr": {
"approvedPushes": "Approved",
"approvedPushes": "Aprovado",
"rejectedPushes": "Rejeitado",
"filters": "Filtros",
"indexers": "Indexers"
"indexers": "Indexadores"
},
"tubearchivist": {
"downloads": "Queue",
"downloads": "Fila de espera",
"videos": "Vídeos",
"channels": "Canais",
"playlists": "Listas"
},
"truenas": {
"load": "Carga do sistema",
"uptime": "Uptime",
"alerts": "Alerts"
"uptime": "Tempo ativo",
"alerts": "Alertas"
},
"pyload": {
"speed": "Velocidade",
"active": "Active",
"queue": "Queue",
"active": "Ativo",
"queue": "Fila de espera",
"total": "Total"
},
"gluetun": {
@@ -579,21 +579,21 @@
"port_forwarded": "Porta Encaminhada"
},
"hdhomerun": {
"channels": "Channels",
"channels": "Canais",
"hd": "HD",
"tunerCount": "Sintonizadores",
"channelNumber": "Canal",
"channelNetwork": "Rede",
"signalStrength": "Potência",
"signalQuality": "Qualidade",
"symbolQuality": "Quality",
"symbolQuality": "Qualidade",
"networkRate": "Bitrate",
"clientIP": "Cliente"
},
"scrutiny": {
"passed": "Aprovado",
"failed": "Failed",
"unknown": "Unknown"
"failed": "Falhou",
"unknown": "Desconhecido"
},
"paperlessngx": {
"inbox": "Caixa de entrada",
@@ -608,18 +608,18 @@
"low_battery": "Bateria Fraca"
},
"nextdns": {
"wait": "Please Wait",
"wait": "Por favor, aguarde",
"no_devices": "Nenhum dado do dispositivo recebido"
},
"mikrotik": {
"cpuLoad": "Carga do CPU",
"memoryUsed": "Memória Utilizada",
"uptime": "Uptime",
"uptime": "Tempo ativo",
"numberOfLeases": "Concessões"
},
"xteve": {
"streams_all": "Todos os Streams",
"streams_active": "Active Streams",
"streams_active": "Streams Ativas",
"streams_xepg": "Canais XEPG"
},
"opendtu": {
@@ -629,7 +629,7 @@
"limit": "Limite"
},
"opnsense": {
"cpu": "CPU Load",
"cpu": "Carga do CPU",
"memory": "Memória Ativa",
"wanUpload": "Envio WAN",
"wanDownload": "WAN Descarga"
@@ -654,9 +654,9 @@
"load": "Carga Média",
"memory": "Uso de memória",
"wanStatus": "Estado WAN",
"up": "Up",
"down": "Down",
"temp": "Temp",
"up": "Ativo",
"down": "Inativo",
"temp": "Temp.",
"disk": "Uso do disco",
"wanIP": "IP WAN"
},
@@ -667,49 +667,49 @@
"memory_usage": "Memória"
},
"immich": {
"users": "Users",
"users": "Usuários",
"photos": "Fotos",
"videos": "Videos",
"videos": "Vídeos",
"storage": "Armazenamento"
},
"uptimekuma": {
"up": "Sites no Ar",
"down": "Sites Fora do Ar",
"uptime": "Uptime",
"uptime": "Tempo ativo",
"incident": "Incidente",
"m": "m"
},
"atsumeru": {
"series": "Series",
"series": "Séries",
"archives": "Arquivos",
"chapters": "Capítulos",
"categories": "Categorias"
},
"komga": {
"libraries": "Bibliotecas",
"series": "Series",
"books": "Books"
"series": "Séries",
"books": "Livros"
},
"diskstation": {
"days": "Days",
"uptime": "Uptime",
"volumeAvailable": "Available"
"days": "Dias",
"uptime": "Tempo ativo",
"volumeAvailable": "Disponível"
},
"mylar": {
"series": "Series",
"series": "Séries",
"issues": "Problemas",
"wanted": "Wanted"
"wanted": "Desejado"
},
"photoprism": {
"albums": "Albums",
"photos": "Photos",
"videos": "Videos",
"albums": "Álbuns",
"photos": "Fotos",
"videos": "Vídeos",
"people": "Pessoa"
},
"fileflows": {
"queue": "Queue",
"processing": "Processing",
"processed": "Processed",
"queue": "Fila de espera",
"processing": "Processando",
"processed": "Processado",
"time": "Hora"
},
"firefly": {
@@ -735,7 +735,7 @@
"size": "Tamanho",
"lastrun": "Ultima Execução",
"nextrun": "Próxima Execução",
"failed": "Failed"
"failed": "Falhou"
},
"unmanic": {
"active_workers": "Workers Ativos",
@@ -752,20 +752,20 @@
"targets_total": "Total de Alvos"
},
"gatus": {
"up": "Sites Up",
"down": "Sites Down",
"uptime": "Uptime"
"up": "Sites no Ar",
"down": "Sites Fora do Ar",
"uptime": "Tempo ativo"
},
"ghostfolio": {
"gross_percent_today": "Today",
"gross_percent_today": "Hoje",
"gross_percent_1y": "Um ano",
"gross_percent_max": "Todo o tempo"
},
"audiobookshelf": {
"podcasts": "Podcasts",
"books": "Books",
"books": "Livros",
"podcastsDuration": "Duração",
"booksDuration": "Duration"
"booksDuration": "Duração"
},
"homeassistant": {
"people_home": "Pessoas em Casa",
@@ -774,23 +774,23 @@
},
"whatsupdocker": {
"monitoring": "Monitorando",
"updates": "Updates"
"updates": "Atualizações"
},
"calibreweb": {
"books": "Books",
"books": "Livros",
"authors": "Autores",
"categories": "Categories",
"series": "Series"
"categories": "Categorias",
"series": "Séries"
},
"jdownloader": {
"downloadCount": "Queue",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size",
"downloadSpeed": "Speed"
"downloadCount": "Fila de espera",
"downloadBytesRemaining": "Restante",
"downloadTotalBytes": "Tamanho",
"downloadSpeed": "Velocidade"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
"seriesCount": "Séries",
"totalFiles": "Arquivos"
},
"azuredevops": {
"result": "Resultado",
@@ -798,21 +798,21 @@
"buildId": "ID Compilação",
"succeeded": "Bem-sucedido",
"notStarted": "Não iniciado",
"failed": "Failed",
"failed": "Falhou",
"canceled": "Cancelado",
"inProgress": "Em Progresso",
"totalPrs": "Total de PRs",
"myPrs": "Minhas PRs",
"approved": "Approved"
"approved": "Aprovado"
},
"gamedig": {
"status": "Status",
"online": "Online",
"offline": "Offline",
"offline": "Desconectado",
"name": "Nome",
"map": "Mapa",
"currentPlayers": "Jogadores atuais",
"players": "Players",
"players": "Jogadores",
"maxPlayers": "Número Máximo de Jogadores",
"bots": "Robôs",
"ping": "Ping"
@@ -825,39 +825,39 @@
},
"mealie": {
"recipes": "Receitas",
"users": "Users",
"categories": "Categories",
"users": "Usuários",
"categories": "Categorias",
"tags": "Marcadores"
},
"openmediavault": {
"downloading": "Baixando",
"total": "Total",
"running": "Running",
"stopped": "Stopped",
"passed": "Passed",
"failed": "Failed"
"running": "Executando",
"stopped": "Parado",
"passed": "Aprovado",
"failed": "Falhou"
},
"openwrt": {
"uptime": "Uptime",
"uptime": "Tempo ativo",
"cpuLoad": "Carga da CPU média (5m)",
"up": "Up",
"down": "Down",
"up": "Ativo",
"down": "Inativo",
"bytesTx": "Transmitido",
"bytesRx": "Received"
"bytesRx": "Recebido"
},
"uptimerobot": {
"status": "Status",
"uptime": "Uptime",
"uptime": "Tempo ativo",
"lastDown": "Última inatividade",
"downDuration": "Duração de inatividade",
"sitesUp": "Sites Up",
"sitesDown": "Sites Down",
"paused": "Paused",
"sitesUp": "Sites no Ar",
"sitesDown": "Sites Fora do Ar",
"paused": "Pausado",
"notyetchecked": "Não conferidos ainda",
"up": "Up",
"up": "Ativo",
"seemsdown": "Parece Desconectado",
"down": "Down",
"unknown": "Unknown"
"down": "Inativo",
"unknown": "Desconhecido"
},
"calendar": {
"inCinemas": "Nos cinemas",
@@ -876,10 +876,10 @@
"totalfilesize": "Tamanho total"
},
"mailcow": {
"domains": "Domains",
"domains": "Domínios",
"mailboxes": "Caixas de e-mail",
"mails": "Mensagens",
"storage": "Storage"
"storage": "Armazenamento"
},
"netdata": {
"warnings": "Alertas",
@@ -888,12 +888,12 @@
"plantit": {
"events": "Eventos",
"plants": "Plantas",
"photos": "Photos",
"photos": "Fotos",
"species": "Espécies"
},
"gitea": {
"notifications": "Notificações",
"issues": "Issues",
"issues": "Problemas",
"pulls": "Solicitações de Envio",
"repositories": "Repositórios"
},
@@ -909,13 +909,13 @@
"galleries": "Galerias",
"performers": "Atores",
"studios": "Estúdios",
"movies": "Movies",
"tags": "Tags",
"movies": "Filmes",
"tags": "Etiquetas",
"oCount": "Contagem 0"
},
"tandoor": {
"users": "Users",
"recipes": "Recipes",
"users": "Usuários",
"recipes": "Receitas",
"keywords": "Palavras-chave"
},
"homebox": {
@@ -923,17 +923,17 @@
"totalWithWarranty": "Com Garantia",
"locations": "Localização",
"labels": "Rótulos",
"users": "Users",
"users": "Usuários",
"totalValue": "Valor Total"
},
"crowdsec": {
"alerts": "Alerts",
"alerts": "Alertas",
"bans": "Banimentos"
},
"wgeasy": {
"connected": "Connected",
"enabled": "Enabled",
"disabled": "Disabled",
"connected": "Conectado",
"enabled": "Ativo",
"disabled": "Desativado",
"total": "Total"
},
"swagdashboard": {
@@ -944,8 +944,8 @@
},
"myspeed": {
"ping": "Ping",
"download": "Download",
"upload": "Upload"
"download": "Baixar",
"upload": "Enviar"
},
"stocks": {
"stocks": "Ações",
@@ -956,17 +956,17 @@
},
"frigate": {
"cameras": "Câmeras",
"uptime": "Uptime",
"version": "Version"
"uptime": "Tempo ativo",
"version": "Versão"
},
"linkwarden": {
"links": "Links",
"collections": "Coleções",
"tags": "Tags"
"tags": "Etiquetas"
},
"zabbix": {
"unclassified": "Não classificado",
"information": "Information",
"information": "Informação",
"warning": "Aviso",
"average": "Médio",
"high": "Alto",
@@ -987,24 +987,24 @@
"tasksInProgress": "Tarefas em Andamento"
},
"headscale": {
"name": "Name",
"address": "Address",
"last_seen": "Last Seen",
"name": "Nome",
"address": "Endereço",
"last_seen": "Visto por último",
"status": "Status",
"online": "Online",
"offline": "Offline"
"offline": "Desconectado"
},
"beszel": {
"name": "Name",
"name": "Nome",
"systems": "Sistemas",
"up": "Up",
"down": "Down",
"paused": "Paused",
"pending": "Pending",
"up": "Ativo",
"down": "Inativo",
"paused": "Pausado",
"pending": "Pendente",
"status": "Status",
"updated": "Updated",
"updated": "Atualizado",
"cpu": "CPU",
"memory": "MEM",
"memory": "MEM.",
"disk": "Disco",
"network": "Rede"
},
@@ -1012,26 +1012,26 @@
"apps": "Aplicativos",
"synced": "Sincronizado",
"outOfSync": "Fora de sincronia",
"healthy": "Healthy",
"healthy": "Saudável",
"degraded": "Degradado",
"progressing": "Progredindo",
"missing": "Missing",
"missing": "Faltando",
"suspended": "Suspenso"
},
"spoolman": {
"loading": "Loading"
"loading": "Carregando"
},
"gitlab": {
"groups": "Grupos",
"issues": "Issues",
"issues": "Problemas",
"merges": "Solicitações de mesclagem",
"projects": "Projetos"
},
"apcups": {
"status": "Status",
"load": "Load",
"bcharge": "Battery Charge",
"timeleft": "Time Left"
"load": "Carga",
"bcharge": "Carga da Bateria",
"timeleft": "Tempo Restante"
},
"karakeep": {
"bookmarks": "Marcadores",
@@ -1039,23 +1039,23 @@
"archived": "Arquivados",
"highlights": "Destaques",
"lists": "Listas",
"tags": "Tags"
"tags": "Etiquetas"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"slskStatus": "Rede",
"connected": "Conectado",
"disconnected": "Desconectado",
"updateStatus": "Atualize",
"update_yes": "Available",
"update_no": "Up to Date",
"update_yes": "Disponível",
"update_no": "Atualizado",
"downloads": "Transferências",
"uploads": "Envios",
"sharedFiles": "Files"
"sharedFiles": "Arquivos"
},
"jellystat": {
"songs": "Songs",
"movies": "Movies",
"episodes": "Episodes",
"songs": "Músicas",
"movies": "Filmes",
"episodes": "Episódios",
"other": "Outro"
},
"checkmk": {
@@ -1064,60 +1064,60 @@
},
"komodo": {
"total": "Total",
"running": "Running",
"stopped": "Stopped",
"down": "Down",
"unhealthy": "Unhealthy",
"unknown": "Unknown",
"servers": "Servers",
"stacks": "Stacks",
"containers": "Containers"
"running": "Executando",
"stopped": "Parado",
"down": "Inativo",
"unhealthy": "Não-saudável",
"unknown": "Desconhecido",
"servers": "Servidores",
"stacks": "Pilhas",
"containers": "Contêineres"
},
"filebrowser": {
"available": "Available",
"used": "Used",
"available": "Disponível",
"used": "Utilizado",
"total": "Total"
},
"wallos": {
"activeSubscriptions": "Subscriptions",
"thisMonthlyCost": "This Month",
"nextMonthlyCost": "Next Month",
"previousMonthlyCost": "Prev. Month",
"nextRenewingSubscription": "Next Payment"
"activeSubscriptions": "Assinaturas",
"thisMonthlyCost": "Este Mês",
"nextMonthlyCost": "Próximo Mês",
"previousMonthlyCost": "Mês Anterior",
"nextRenewingSubscription": "Próximo Pagamento"
},
"unraid": {
"STARTED": "Started",
"STOPPED": "Stopped",
"NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",
"SWAP_DSBL": "Swap Disable",
"INVALID_EXPANSION": "Invalid Expansion",
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
"NO_DATA_DISKS": "No Data Disks",
"notifications": "Notifications",
"STARTED": "Iniciado",
"STOPPED": "Parado",
"NEW_ARRAY": "Nova Array",
"RECON_DISK": "Reconstruindo Disco",
"DISABLE_DISK": "Disco Desativado",
"SWAP_DSBL": "Swap Desabilitado",
"INVALID_EXPANSION": "Expansão Inválida",
"PARITY_NOT_BIGGEST": "Paridade Não É O Maior Disco",
"TOO_MANY_MISSING_DISKS": "Muitos Discos Ausentes",
"NEW_DISK_TOO_SMALL": "Novo Disco É Muito Pequeno",
"NO_DATA_DISKS": "Sem Discos de Dados",
"notifications": "Notificações",
"status": "Status",
"cpu": "CPU",
"memoryUsed": "Memory Used",
"memoryAvailable": "Memory Available",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"poolUsed": "{{pool}} Used",
"poolFree": "{{pool}} Free"
"memoryUsed": "Memória Utilizada",
"memoryAvailable": "Memória Disponível",
"arrayUsed": "Array Utilizado",
"arrayFree": "Array Disponível",
"poolUsed": "{{pool}} Utilizado",
"poolFree": "{{pool}} Livre"
},
"backrest": {
"num_plans": "Plans",
"num_success_30": "Successes",
"num_failure_30": "Failures",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
"num_plans": "Planos",
"num_success_30": "Sucessos",
"num_failure_30": "Falhas",
"num_success_latest": "Executando com sucesso",
"num_failure_latest": "Falhando",
"bytes_added_30": "Bytes Adicionados"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
"songs": "Músicas",
"time": "Tempo",
"artists": "Artistas"
}
}

View File

@@ -1,16 +1,47 @@
import classNames from "classnames";
import { useTranslation } from "next-i18next";
import { useContext, useMemo } from "react";
export default function Block({ value, label }) {
import { BlockHighlightContext } from "./highlight-context";
import { evaluateHighlight, getHighlightClass } from "utils/highlights";
export default function Block({ value, label, field }) {
const { t } = useTranslation();
const highlightConfig = useContext(BlockHighlightContext);
const highlight = useMemo(() => {
if (!highlightConfig) return null;
const labels = Array.isArray(label) ? label : [label];
const candidates = [];
if (typeof field === "string") candidates.push(field);
for (const candidateLabel of labels) {
if (typeof candidateLabel === "string") candidates.push(candidateLabel);
}
for (const candidate of candidates) {
const result = evaluateHighlight(candidate, value, highlightConfig);
if (result) return result;
}
return null;
}, [field, label, value, highlightConfig]);
const highlightClass = useMemo(() => {
if (!highlight?.level) return undefined;
return getHighlightClass(highlight.level, highlightConfig);
}, [highlight, highlightConfig]);
return (
<div
className={classNames(
"bg-theme-200/50 dark:bg-theme-900/20 rounded-sm m-1 flex-1 flex flex-col items-center justify-center text-center p-1",
value === undefined ? "animate-pulse" : "",
highlightClass,
"service-block",
)}
data-highlight-level={highlight?.level}
data-highlight-source={highlight?.source}
>
<div className="font-thin text-sm">{value === undefined || value === null ? "-" : value}</div>
<div className="font-bold text-xs uppercase">{t(label)}</div>

View File

@@ -1,7 +1,10 @@
import { useContext } from "react";
import { useContext, useMemo } from "react";
import { SettingsContext } from "utils/contexts/settings";
import Error from "./error";
import { BlockHighlightContext } from "./highlight-context";
import { buildHighlightConfig } from "utils/highlights";
const ALIASED_WIDGETS = {
pialert: "netalertx",
@@ -11,6 +14,11 @@ const ALIASED_WIDGETS = {
export default function Container({ error = false, children, service }) {
const { settings } = useContext(SettingsContext);
const highlightConfig = useMemo(
() => buildHighlightConfig(settings?.blockHighlights, service?.widget?.highlight, service?.widget?.type),
[settings?.blockHighlights, service?.widget?.highlight, service?.widget?.type],
);
if (error) {
if (settings.hideErrors || service.widget.hide_errors) {
return null;
@@ -51,6 +59,11 @@ export default function Container({ error = false, children, service }) {
}),
);
}
const content = <div className="relative flex flex-row w-full service-container">{visibleChildren}</div>;
return <div className="relative flex flex-row w-full service-container">{visibleChildren}</div>;
if (!highlightConfig) {
return content;
}
return <BlockHighlightContext.Provider value={highlightConfig}>{content}</BlockHighlightContext.Provider>;
}

View File

@@ -0,0 +1,3 @@
import { createContext } from "react";
export const BlockHighlightContext = createContext(null);

View File

@@ -55,8 +55,7 @@ export default function Version({ disableUpdateCheck = false }) {
</span>
{!validate(version)
? null
: releaseData &&
latestRelease &&
: latestRelease &&
compareVersions(latestRelease.tag_name, version) > 0 && (
<a
href={latestRelease.html_url}

View File

@@ -113,7 +113,7 @@ export default function Widget({ options }) {
<Resource
icon={FaMemory}
value={t("common.bytes", {
value: data.mem.free,
value: data.mem.available,
maximumFractionDigits: 1,
binary: true,
})}

View File

@@ -425,7 +425,7 @@ function Home({ initialSettings }) {
<div
className={classNames(
settings.fullWidth ? "" : "container",
"relative m-auto flex flex-col justify-start z-10 h-full",
"relative m-auto flex flex-col justify-start z-10 h-full min-h-screen",
)}
>
<QuickLaunch

View File

@@ -254,6 +254,7 @@ export function cleanServiceGroups(groups) {
// all widgets
fields,
hideErrors,
highlight,
type,
// azuredevops
@@ -441,6 +442,21 @@ export function cleanServiceGroups(groups) {
index,
};
if (highlight) {
let parsedHighlight = highlight;
if (typeof highlight === "string") {
try {
parsedHighlight = JSON.parse(highlight);
} catch (e) {
logger.error("Invalid highlight configuration detected in config for service '%s'", service.name);
parsedHighlight = null;
}
}
if (parsedHighlight && typeof parsedHighlight === "object") {
widget.highlight = parsedHighlight;
}
}
if (type === "azuredevops") {
if (userEmail) widget.userEmail = userEmail;
if (repositoryId) widget.repositoryId = repositoryId;

257
src/utils/highlights.js Normal file
View File

@@ -0,0 +1,257 @@
const DEFAULT_LEVEL_CLASSES = {
good: "bg-emerald-500/40 text-emerald-950 dark:bg-emerald-900/60 dark:text-emerald-400",
warn: "bg-amber-300/30 text-amber-900 dark:bg-amber-900/30 dark:text-amber-200",
danger: "bg-rose-700/45 text-rose-200 dark:bg-rose-950/70 dark:text-rose-400",
};
const normalizeFieldKeys = (fields, widgetType) => {
if (!fields || typeof fields !== "object") return {};
return Object.entries(fields).reduce((acc, [key, value]) => {
if (value === null || value === undefined) return acc;
if (typeof key !== "string") return acc;
const trimmedKey = key.trim();
if (trimmedKey === "") return acc;
acc[trimmedKey] = value;
if (widgetType && !trimmedKey.includes(".")) {
const namespacedKey = `${widgetType}.${trimmedKey}`;
if (!(namespacedKey in acc)) {
acc[namespacedKey] = value;
}
}
return acc;
}, {});
};
export const buildHighlightConfig = (globalConfig, widgetConfig, widgetType) => {
const levels = {
...DEFAULT_LEVEL_CLASSES,
...(globalConfig?.levels || {}),
...(widgetConfig?.levels || {}),
};
const { levels: _levels, ...fields } = widgetConfig || {};
const normalizedFields = normalizeFieldKeys(fields, widgetType);
const hasLevels = Object.values(levels).some(Boolean);
const hasFields = Object.keys(normalizedFields).length > 0;
if (!hasLevels && !hasFields) return null;
return { levels, fields: normalizedFields };
};
const NUMERIC_OPERATORS = {
gt: (value, target) => value > target,
gte: (value, target) => value >= target,
lt: (value, target) => value < target,
lte: (value, target) => value <= target,
eq: (value, target) => value === target,
ne: (value, target) => value !== target,
};
const STRING_OPERATORS = {
equals: (value, target, caseSensitive) =>
caseSensitive ? value === target : value.toLowerCase() === target.toLowerCase(),
includes: (value, target, caseSensitive) =>
caseSensitive ? value.includes(target) : value.toLowerCase().includes(target.toLowerCase()),
startsWith: (value, target, caseSensitive) =>
caseSensitive ? value.startsWith(target) : value.toLowerCase().startsWith(target.toLowerCase()),
endsWith: (value, target, caseSensitive) =>
caseSensitive ? value.endsWith(target) : value.toLowerCase().endsWith(target.toLowerCase()),
};
const toNumber = (value) => {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const trimmed = value.trim();
const candidate = Number(trimmed);
if (!Number.isNaN(candidate)) return candidate;
}
return undefined;
};
const parseNumericValue = (value) => {
if (value === null || value === undefined) return undefined;
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) return undefined;
const direct = Number(trimmed);
if (!Number.isNaN(direct)) return direct;
const compact = trimmed.replace(/\s+/g, "");
if (!compact || !/^[-+]?[0-9.,]+$/.test(compact)) return undefined;
const commaCount = (compact.match(/,/g) || []).length;
const dotCount = (compact.match(/\./g) || []).length;
if (commaCount && dotCount) {
const lastComma = compact.lastIndexOf(",");
const lastDot = compact.lastIndexOf(".");
if (lastComma > lastDot) {
const asDecimal = compact.replace(/\./g, "").replace(/,/g, ".");
const parsed = Number(asDecimal);
return Number.isNaN(parsed) ? undefined : parsed;
}
const asThousands = compact.replace(/,/g, "");
const parsed = Number(asThousands);
return Number.isNaN(parsed) ? undefined : parsed;
}
if (commaCount) {
const parts = compact.split(",");
if (commaCount === 1 && parts[1]?.length <= 2) {
const parsed = Number(compact.replace(",", "."));
if (!Number.isNaN(parsed)) return parsed;
}
const isGrouped = parts.length > 1 && parts.slice(1).every((part) => part.length === 3);
if (isGrouped) {
const parsed = Number(compact.replace(/,/g, ""));
if (!Number.isNaN(parsed)) return parsed;
}
return undefined;
}
if (dotCount) {
const parts = compact.split(".");
if (dotCount === 1 && parts[1]?.length <= 2) {
const parsed = Number(compact);
if (!Number.isNaN(parsed)) return parsed;
}
const isGrouped = parts.length > 1 && parts.slice(1).every((part) => part.length === 3);
if (isGrouped) {
const parsed = Number(compact.replace(/\./g, ""));
if (!Number.isNaN(parsed)) return parsed;
}
const parsed = Number(compact);
return Number.isNaN(parsed) ? undefined : parsed;
}
const parsed = Number(compact);
return Number.isNaN(parsed) ? undefined : parsed;
}
if (typeof value === "object" && value !== null && "props" in value) {
return undefined;
}
return undefined;
};
const evaluateNumericRule = (value, rule) => {
if (!rule || typeof rule !== "object") return false;
const operator = rule.when && NUMERIC_OPERATORS[rule.when];
const numericValue = toNumber(rule.value);
if (operator && numericValue !== undefined) {
const passes = operator(value, numericValue);
return rule.negate ? !passes : passes;
}
if (rule.when === "between") {
const min = toNumber(rule.min ?? rule.value?.min);
const max = toNumber(rule.max ?? rule.value?.max);
if (min === undefined && max === undefined) return false;
const lowerBound = min ?? Number.NEGATIVE_INFINITY;
const upperBound = max ?? Number.POSITIVE_INFINITY;
const passes = value >= lowerBound && value <= upperBound;
return rule.negate ? !passes : passes;
}
if (rule.when === "outside") {
const min = toNumber(rule.min ?? rule.value?.min);
const max = toNumber(rule.max ?? rule.value?.max);
if (min === undefined && max === undefined) return false;
const passes = value < (min ?? Number.NEGATIVE_INFINITY) || value > (max ?? Number.POSITIVE_INFINITY);
return rule.negate ? !passes : passes;
}
return false;
};
const evaluateStringRule = (value, rule) => {
if (!rule || typeof rule !== "object") return false;
if (rule.when === "regex" && typeof rule.value === "string") {
try {
const flags = rule.flags || (rule.caseSensitive ? "" : "i");
const regex = new RegExp(rule.value, flags);
const passes = regex.test(value);
return rule.negate ? !passes : passes;
} catch (error) {
return false;
}
}
const operator = rule.when && STRING_OPERATORS[rule.when];
if (!operator || typeof rule.value !== "string") return false;
const passes = operator(value, rule.value, Boolean(rule.caseSensitive));
return rule.negate ? !passes : passes;
};
const ensureArray = (value) => {
if (Array.isArray(value)) return value;
if (value === undefined || value === null) return [];
return [value];
};
const findHighlightLevel = (ruleSet, numericValue, stringValue) => {
const { numeric, string } = ruleSet;
if (numeric && numericValue !== undefined) {
const numericRules = ensureArray(numeric);
const numericCandidates = Array.isArray(numericValue) ? numericValue : [numericValue];
for (const candidate of numericCandidates) {
for (const rule of numericRules) {
if (rule?.level && evaluateNumericRule(candidate, rule)) {
return { level: rule.level, source: "numeric", rule };
}
}
}
}
if (string && stringValue !== undefined) {
const stringRules = ensureArray(string);
for (const rule of stringRules) {
if (rule?.level && evaluateStringRule(stringValue, rule)) {
return { level: rule.level, source: "string", rule };
}
}
}
return null;
};
export const evaluateHighlight = (fieldKey, value, highlightConfig) => {
if (!highlightConfig || !fieldKey) return null;
const { fields } = highlightConfig;
if (!fields || typeof fields !== "object") return null;
const ruleSet = fields[fieldKey];
if (!ruleSet) return null;
const numericValue = parseNumericValue(value);
let stringValue;
if (typeof value === "string") {
stringValue = value;
} else if (typeof value === "number" || typeof value === "bigint") {
stringValue = String(value);
} else if (typeof value === "boolean") {
stringValue = value ? "true" : "false";
}
const normalizedString = typeof stringValue === "string" ? stringValue.trim() : stringValue;
return findHighlightLevel(ruleSet, numericValue, normalizedString);
};
export const getHighlightClass = (level, highlightConfig) => {
if (!level || !highlightConfig) return undefined;
return highlightConfig.levels?.[level];
};
export const getDefaultHighlightLevels = () => DEFAULT_LEVEL_CLASSES;

View File

@@ -24,10 +24,11 @@ function buildResponse(plans) {
plans.forEach((plan) => {
const statuses = plan?.recentBackups?.status;
// See https://github.com/garethgeorge/backrest/blob/4357295a17cb2e71639473c9929a060c4dd1b624/proto/v1/operations.proto#L78-L87
if (Array.isArray(statuses) && statuses.length > 0) {
if (statuses[0] === "STATUS_SUCCESS") {
numSuccessLatest++;
} else {
} else if (statuses[0] === "STATUS_ERROR") {
numFailureLatest++;
}
}

View File

@@ -205,13 +205,14 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const enableNowPlaying = service.widget?.enableNowPlaying ?? true;
const {
data: sessionsData,
error: sessionsError,
mutate: sessionMutate,
} = useWidgetAPI(widget, "Sessions", {
refreshInterval: 5000,
} = useWidgetAPI(widget, enableNowPlaying ? "Sessions" : "", {
refreshInterval: enableNowPlaying ? 5000 : undefined,
});
const { data: countData, error: countError } = useWidgetAPI(widget, "Count", {
@@ -239,13 +240,12 @@ export default function Component({ service }) {
}
const enableBlocks = service.widget?.enableBlocks;
const enableNowPlaying = service.widget?.enableNowPlaying ?? true;
const enableMediaControl = service.widget?.enableMediaControl !== false; // default is true
const enableUser = !!service.widget?.enableUser; // default is false
const expandOneStreamToTwoRows = service.widget?.expandOneStreamToTwoRows !== false; // default is true
const showEpisodeNumber = !!service.widget?.showEpisodeNumber; // default is false
if (!sessionsData || !countData) {
if ((enableNowPlaying && !sessionsData) || !countData) {
return (
<>
{enableBlocks && <CountBlocks service={service} countData={null} />}

View File

@@ -27,7 +27,7 @@ export default function Component({ service }) {
useEffect(() => {
if (data) {
setDataPoints((prevDataPoints) => {
const newDataPoints = [...prevDataPoints, { a: data.used, b: data.free }];
const newDataPoints = [...prevDataPoints, { a: data.used, b: data.available }];
if (newDataPoints.length > pointsLimit) {
newDataPoints.shift();
}
@@ -67,10 +67,10 @@ export default function Component({ service }) {
{data && !error && (
<Block position="bottom-3 left-3">
{data.free && chart && (
{data.available && chart && (
<div className="text-xs opacity-50">
{t("common.bytes", {
value: data.free,
value: data.available,
maximumFractionDigits: 1,
binary: true,
})}{" "}
@@ -93,10 +93,10 @@ export default function Component({ service }) {
{!chart && (
<Block position="top-3 right-3">
{data.free && (
{data.available && (
<div className="text-xs opacity-50">
{t("common.bytes", {
value: data.free,
value: data.available,
maximumFractionDigits: 1,
binary: true,
})}{" "}

View File

@@ -33,14 +33,6 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block
label="myspeed.ping"
value={t("common.ms", {
value: data[0].ping,
style: "unit",
unit: "millisecond",
})}
/>
<Block
label="myspeed.download"
value={t("common.bitrate", {
@@ -55,6 +47,14 @@ export default function Component({ service }) {
decimals: 2,
})}
/>
<Block
label="myspeed.ping"
value={t("common.ms", {
value: data[0].ping,
style: "unit",
unit: "millisecond",
})}
/>
</Container>
);
}

View File

@@ -66,7 +66,7 @@ export default async function omadaProxyHandler(req, res) {
const controllerVersionMajor = parseInt(controllerVersion.split(".")[0], 10);
if (![3, 4, 5].includes(controllerVersionMajor)) {
if (![3, 4, 5, 6].includes(controllerVersionMajor)) {
return res.status(500).json({ error: { message: "Error determining controller version", data } });
}
@@ -80,6 +80,7 @@ export default async function omadaProxyHandler(req, res) {
loginUrl = `${url}/api/v2/login`;
break;
case 5:
case 6:
loginUrl = `${url}/${cId}/api/v2/login`;
break;
default:
@@ -122,6 +123,7 @@ export default async function omadaProxyHandler(req, res) {
sitesUrl = `${url}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
break;
case 5:
case 6:
sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
break;
default:
@@ -207,8 +209,8 @@ export default async function omadaProxyHandler(req, res) {
connectedAp = siteResponseData.result.connectedAp;
activeUser = siteResponseData.result.activeUser;
alerts = siteResponseData.result.alerts;
} else if (controllerVersionMajor === 4 || controllerVersionMajor === 5) {
const siteName = controllerVersionMajor === 5 ? site.id : site.key;
} else if ([4, 5, 6].includes(controllerVersionMajor)) {
const siteName = controllerVersionMajor > 4 ? site.id : site.key;
const siteStatsUrl =
controllerVersionMajor === 4
? `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`

View File

@@ -52,6 +52,7 @@ export default function Component({ service }) {
let status;
let uptime = 0;
let logIndex = 0;
const hasLogs = Array.isArray(monitor.logs) && monitor.logs.length > 0;
switch (monitor.status) {
case 0:
@@ -62,7 +63,7 @@ export default function Component({ service }) {
break;
case 2:
status = t("uptimerobot.up");
uptime = t("common.duration", { value: monitor.logs[0].duration });
uptime = t("common.duration", { value: hasLogs ? monitor.logs[0].duration : 0 });
logIndex = 1;
break;
case 8:
@@ -76,14 +77,14 @@ export default function Component({ service }) {
break;
}
const lastDown = new Date(monitor.logs[logIndex].datetime * 1000).toLocaleString();
const downDuration = t("common.duration", { value: monitor.logs[logIndex].duration });
const hideDown = logIndex === 1 && monitor.logs[logIndex].type !== 1;
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>