Compare commits

..

1 Commits

Author SHA1 Message Date
Crowdin Bot
98203b99e7 New Crowdin translations by GitHub Action 2025-09-26 12:13:37 +00:00
69 changed files with 826 additions and 1128 deletions

View File

@@ -38,7 +38,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v5
with:
node-version: 20
cache: 'pnpm'
@@ -96,7 +96,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v5
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-container-stats).
Also see the settings for [show docker stats](settings.md#show-docker-stats).

View File

@@ -178,32 +178,3 @@ See [ClusterRole and ClusterRoleBinding](../installation/k8s.md#clusterrole-and-
## Caveats
Similarly to Docker service discovery, there currently is no rigid ordering to discovered services and discovered services will be displayed above those specified in the `services.yaml`.
## Adding extra configuration files
Some Homepage features (for example, [Proxmox](../configs/proxmox.md)) require additional configuration files such as `proxmox.yaml`.
When running Homepage on Kubernetes, these files must be provided via a `ConfigMap` and mounted into the container at `/app/config`.
### ConfigMap example
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: homepage
data:
proxmox.yaml: |
pve:
url: https://proxmox.host.or.ip:8006
token: username@pam!Token ID
secret: secret
```
Mount the file into `/app/config` by updating the `Deployment`:
```yaml
volumeMounts:
- mountPath: /app/config/proxmox.yaml
name: homepage-config
subPath: proxmox.yaml
```

View File

@@ -4,10 +4,10 @@ description: Proxmox Configuration
---
The Proxmox connection is configured in the `proxmox.yaml` file. See [Create token](#create-token) section below for details on how to generate the required API token.
To configure multiple nodes, ensure the key name in the `proxmox.yaml` matches the `proxmoxNode` field used in your service configuration.
You can configure multiple nodes - be sure to use the exact `proxmoxNode` identifier!
```yaml
pve: # must match your actual Proxmox node name
pve:
url: https://proxmox.host.or.ip:8006
token: username@pam!Token ID
secret: secret

View File

@@ -118,47 +118,6 @@ 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,20 +109,6 @@ 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:
@@ -500,9 +486,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 Container Stats
## Show Docker Stats
You can show all docker or proxmox stats expanded in `settings.yaml`:
You can show all docker stats expanded in `settings.yaml`:
```yaml
showStats: true

View File

@@ -62,4 +62,3 @@ 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

@@ -16,5 +16,4 @@ widget:
username: username
password: password
enableLeechProgress: true # optional, defaults to false
enableLeechSize: true # optional, defaults to false
```

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 **ADMIN** role: [Managing API Keys](https://docs.unraid.net/go/managing-api-keys)
- API key with the **GUEST** (read only) 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","arrayFree","arrayUsedSpace","arrayUsedPercent","status","pool1UsedSpace","pool1FreeSpace","pool1UsedPercent","pool2UsedSpace","pool2FreeSpace","pool2UsedPercent","pool3UsedSpace","pool3FreeSpace","pool3UsedPercent","pool4UsedSpace","pool4FreeSpace","pool4UsedPercent"]`
**Allowed fields:** `["cpu","memoryPercent","memoryAvailable","memoryUsed","notifications","arrayFreeSpace","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.6.0",
"version": "1.5.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -11,14 +11,14 @@
"telemetry": "next telemetry disable"
},
"dependencies": {
"@headlessui/react": "^2.2.9",
"@headlessui/react": "^2.2.7",
"@kubernetes/client-node": "^1.0.0",
"classnames": "^2.5.1",
"compare-versions": "^6.1.1",
"dockerode": "^4.0.7",
"follow-redirects": "^1.15.11",
"gamedig": "^5.3.2",
"i18next": "^25.5.3",
"gamedig": "^5.3.1",
"i18next": "^24.2.3",
"ical.js": "^2.1.0",
"js-yaml": "^4.1.0",
"json-rpc-2.0": "^1.7.0",
@@ -28,8 +28,8 @@
"next": "^15.5.2",
"next-i18next": "^12.1.0",
"ping": "^0.4.4",
"pretty-bytes": "^7.1.0",
"raw-body": "^3.0.1",
"pretty-bytes": "^6.1.1",
"raw-body": "^3.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.5.3",
@@ -44,18 +44,18 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/postcss": "^4.0.9",
"eslint": "^9.25.1",
"eslint-config-next": "^15.2.4",
"eslint-config-prettier": "^10.1.8",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-organize-imports": "^4.1.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.0.9",
"typescript": "^5.7.3"

663
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Slaag",
"num_failure_latest": "Mislukking",
"bytes_added_30": "Grepe bygevoeg"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1098,10 +1098,10 @@
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
"NO_DATA_DISKS": "No Data Disks",
"notifications": "Notifications",
"status": "Status",
"cpu": "CPU",
"memoryUsed": "Memory Used",
"memoryAvailable": "Memory Available",
"status": "État",
"cpu": "UCT",
"memoryUsed": "Mémoire Utilisé",
"memoryAvailable": "Mémoire Disponible",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"poolUsed": "{{pool}} Used",
@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -61,16 +61,16 @@
"wlan_devices": "WLAN Eszközök",
"lan_users": "LAN Felhasználók",
"wlan_users": "WLAN Felhasználók",
"up": "UP",
"up": "FUT",
"down": "ÁLL",
"wait": "Please wait",
"wait": "Kérjük várjon",
"empty_data": "Az alrendszer állapota ismeretlen"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "MEM",
"cpu": "CPU",
"cpu": "Processzor",
"running": "Futó",
"offline": "Nem elérhető",
"error": "Hiba",
@@ -93,8 +93,8 @@
"http_status": "HTTP állapot",
"error": "Hiba",
"response": "Válasz",
"down": "Down",
"up": "Up",
"down": "Leállt",
"up": "Fut",
"not_available": "Nem elérhető"
},
"emby": {
@@ -108,10 +108,10 @@
"songs": "Zeneszám"
},
"esphome": {
"offline": "Offline",
"offline_alt": "Offline",
"offline": "Nem elérhető",
"offline_alt": "Nem elérhető",
"online": "Csatlakozva",
"total": "Total",
"total": "Összes",
"unknown": "Ismeretlen"
},
"evcc": {
@@ -133,7 +133,7 @@
"unread": "Olvasatlan"
},
"fritzbox": {
"connectionStatus": "Status",
"connectionStatus": "Státusz",
"connectionStatusUnconfigured": "Nem beállított",
"connectionStatusConnecting": "Kapcsolódás",
"connectionStatusAuthenticating": "Hitelesítés",
@@ -141,16 +141,16 @@
"connectionStatusDisconnecting": "Kapcsolat bontása",
"connectionStatusDisconnected": "Kapcsolat bontva",
"connectionStatusConnected": "Csatlakozva",
"uptime": "Uptime",
"uptime": "Működési idő",
"maxDown": "Max let.",
"maxUp": "Max felt.",
"down": "Down",
"up": "Up",
"down": "Leállt",
"up": "Fut",
"received": "Fogadott",
"sent": "Küldött",
"externalIPAddress": "Külső IP cím",
"externalIPv6Address": "Ext. IPv6",
"externalIPv6Prefix": "Ext. IPv6-Prefix"
"externalIPv6Address": "Küls. IPv6",
"externalIPv6Prefix": "Küls. IPv6-Prefix"
},
"caddy": {
"upstreams": "Upstreamek",
@@ -168,17 +168,17 @@
"passes": "Engedélyek"
},
"tautulli": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"playing": "Lejátszás",
"transcoding": "Transzkódolás",
"bitrate": "Bitráta",
"no_active": "Nincs aktív lejátszás",
"plex_connection_error": "Plex kapcsolat ellenőrzése"
},
"omada": {
"connectedAp": "Csatlakoztatott AP-k",
"activeUser": "Aktív eszközök",
"alerts": "Riasztások",
"connectedGateways": "Connected gateways",
"connectedGateways": "Csatlakoztatott gateway-ek",
"connectedSwitches": "Csatlakoztatott switch-ek"
},
"nzbget": {
@@ -189,11 +189,11 @@
"plex": {
"streams": "Aktív Stream-ek",
"albums": "Albumok",
"movies": "Movies",
"movies": "Filmek",
"tv": "TV műsorok"
},
"sabnzbd": {
"rate": "Rate",
"rate": "Ráta",
"queue": "Sor",
"timeleft": "Hátralévő idő"
},
@@ -233,34 +233,34 @@
"cachemissbytes": "Gyorsítótárban Hibás Bitek"
},
"downloadstation": {
"download": "Download",
"upload": "Upload",
"download": "Letöltés",
"upload": "Feltöltés",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Keresett",
"queued": "Sorban áll",
"series": "Series",
"queue": "Queue",
"unknown": "Unknown"
"series": "Sorozatok",
"queue": "Várólista",
"unknown": "Ismeretlen"
},
"radarr": {
"wanted": "Wanted",
"wanted": "Keresett",
"missing": "Hiányzik",
"queued": "Queued",
"movies": "Movies",
"queue": "Queue",
"unknown": "Unknown"
"queued": "Sorban áll",
"movies": "Filmek",
"queue": "Várólista",
"unknown": "Ismeretlen"
},
"lidarr": {
"wanted": "Wanted",
"queued": "Queued",
"wanted": "Keresett",
"queued": "Sorban áll",
"artists": "Előadók"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"wanted": "Keresett",
"queued": "Sorban áll",
"books": "Könyvek"
},
"bazarr": {
@@ -273,20 +273,20 @@
"available": "Elérhető"
},
"jellyseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"issues": "Open Issues"
"pending": "Függőben lévő",
"approved": "Jóváhagyott",
"available": "Elérhető",
"issues": "Nyitott problémák"
},
"overseerr": {
"pending": "Pending",
"pending": "Függőben lévő",
"processing": "Feldolgozás",
"approved": "Approved",
"available": "Available"
"approved": "Jóváhagyott",
"available": "Elérhető"
},
"netalertx": {
"total": "Total",
"connected": "Connected",
"total": "Összes",
"connected": "Csatlakoztatott",
"new_devices": "Új eszközök",
"down_alerts": "Leállási riasztások"
},
@@ -297,26 +297,26 @@
"gravity": "Gravitáció"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"queries": "Lekérdezések",
"blocked": "Blokkolt",
"filtered": "Szűrt",
"latency": "Késleltetés"
},
"speedtest": {
"upload": "Upload",
"download": "Download",
"upload": "Feltöltés",
"download": "Letöltés",
"ping": "Ping"
},
"portainer": {
"running": "Running",
"running": "Folyamatban",
"stopped": "Megállított",
"total": "Total"
"total": "Összes"
},
"suwayomi": {
"download": "Downloaded",
"download": "Letöltött",
"nondownload": "Nem Letöltött",
"read": "Read",
"unread": "Unread",
"read": "Olvasott",
"unread": "Olvasatlan",
"downloadedread": "Letöltött & Olvasott",
"downloadedunread": "Letöltött & Olvasatlan",
"nondownloadedread": "Nem Letöltött & Olvasatlan",
@@ -337,7 +337,7 @@
"ago": "{{value}} Ezelőtt"
},
"technitium": {
"totalQueries": "Queries",
"totalQueries": "Lekérdezések",
"totalNoError": "Sikerek",
"totalServerFailure": "Hibák",
"totalNxDomain": "NX Domainek",
@@ -345,12 +345,12 @@
"totalAuthoritative": "Irányadó",
"totalRecursive": "Rekurzív",
"totalCached": "Gyorsítótárazott",
"totalBlocked": "Blocked",
"totalBlocked": "Blokkolt",
"totalDropped": "Eldobott",
"totalClients": "Kliensek"
},
"tdarr": {
"queue": "Queue",
"queue": "Várólista",
"processed": "Feldolgozott",
"errored": "Hibás",
"saved": "Mentett"
@@ -361,19 +361,19 @@
"middleware": "Közvetítő"
},
"trilium": {
"version": "Version",
"notesCount": "Notes",
"dbSize": "Database Size",
"unknown": "Unknown"
"version": "Verzió",
"notesCount": "Jegyzetek",
"dbSize": "Adatbázis mérete",
"unknown": "Ismeretlen"
},
"navidrome": {
"nothing_streaming": "No Active Streams",
"nothing_streaming": "Nincs aktív lejátszás",
"please_wait": "Kérjük Várjon"
},
"npm": {
"enabled": "Bekapcsolva",
"disabled": "Kikapcsolva",
"total": "Total"
"total": "Összes"
},
"coinmarketcap": {
"configure": "Állíts be egy vagy több Cryptovalutát a követéshez",
@@ -384,73 +384,73 @@
},
"gotify": {
"apps": "Applikációk",
"clients": "Clients",
"clients": "Kliensek",
"messages": "Üzenetek"
},
"prowlarr": {
"enableIndexers": "Indexerek",
"numberOfGrabs": "Fogott",
"numberOfQueries": "Queries",
"numberOfQueries": "Lekérdezések",
"numberOfFailGrabs": "Hibás fogások",
"numberOfFailQueries": "Hibás lekérdezések"
},
"jackett": {
"configured": "Beállított",
"errored": "Errored"
"errored": "Hibák"
},
"strelaysrv": {
"numActiveSessions": "Munkamenetek",
"numConnections": "Csatlakozások",
"dataRelayed": "Átirányított",
"transferRate": "Rate"
"transferRate": "Ráta"
},
"mastodon": {
"user_count": "Users",
"user_count": "Felhasználók",
"status_count": "Posztok",
"domain_count": "Domainek"
},
"medusa": {
"wanted": "Wanted",
"queued": "Queued",
"series": "Series"
"wanted": "Keresett",
"queued": "Sorban áll",
"series": "Sorozatok"
},
"minecraft": {
"players": "Lejátszók",
"version": "Verzió",
"status": "Status",
"up": "Online",
"down": "Offline"
"status": "Státusz",
"up": "Kapcsolódva",
"down": "Nem elérhető"
},
"miniflux": {
"read": "Olvasott",
"unread": "Unread"
"unread": "Olvasatlan"
},
"authentik": {
"users": "Users",
"users": "Felhasználók",
"loginsLast24H": "Bejelentkezések (24 óra)",
"failedLoginsLast24H": "Sikertelen bejelentkezések (24h)"
},
"proxmox": {
"mem": "MEM",
"cpu": "CPU",
"cpu": "Processzor",
"lxc": "LXC-k",
"vms": "VM-ek"
},
"glances": {
"cpu": "CPU",
"load": "Load",
"wait": "Please wait",
"temp": "TEMP",
"cpu": "Processzor",
"load": "Terhelés",
"wait": "Kérem várjon",
"temp": "HŐM",
"_temp": "Hőmérséklet",
"warn": "Figyelmeztet",
"uptime": "UP",
"total": "Total",
"free": "Free",
"used": "Used",
"days": "d",
"hours": "h",
"uptime": "FUT",
"total": "Összes",
"free": "Szabad",
"used": "Felhasznált",
"days": "n",
"hours": "ó",
"crit": "Kritikus",
"read": "Read",
"read": "Olvasott",
"write": "Írás",
"gpu": "GPU",
"mem": "Memória",
@@ -471,57 +471,57 @@
"1-day": "Többnyire napos",
"1-night": "Többnyire derült",
"2-day": "Részben felhős",
"2-night": "Partly Cloudy",
"2-night": "Részben felhős",
"3-day": "Felhős",
"3-night": "Cloudy",
"3-night": "Felhős",
"45-day": "Ködös",
"45-night": "Foggy",
"48-day": "Foggy",
"48-night": "Foggy",
"45-night": "Ködös",
"48-day": "Ködös",
"48-night": "Ködös",
"51-day": "Enyhe szitálás",
"51-night": "Light Drizzle",
"51-night": "Enyhe szitálás",
"53-day": "Szitálás",
"53-night": "Drizzle",
"53-night": "Szitálás",
"55-day": "Erős szitálás",
"55-night": "Heavy Drizzle",
"55-night": "Erős szitálás",
"56-day": "Enyhe fagyos szitálás",
"56-night": "Light Freezing Drizzle",
"56-night": "Enyhe fagyos szitálás",
"57-day": "Fagyos szitálás",
"57-night": "Freezing Drizzle",
"57-night": "Fagyos szitálás",
"61-day": "Enyhe eső",
"61-night": "Light Rain",
"61-night": "Enyhe eső",
"63-day": "Eső",
"63-night": "Rain",
"63-night": "Eső",
"65-day": "Heves eső",
"65-night": "Heavy Rain",
"65-night": "Heves eső",
"66-day": "Ónos eső",
"66-night": "Freezing Rain",
"67-day": "Freezing Rain",
"67-night": "Freezing Rain",
"66-night": "Ónos eső",
"67-day": "Ónos eső",
"67-night": "Ónos eső",
"71-day": "Enyhe havazás",
"71-night": "Light Snow",
"71-night": "Enyhe havazás",
"73-day": "Hó",
"73-night": "Snow",
"73-night": "Havazás",
"75-day": "Erős havazás",
"75-night": "Heavy Snow",
"75-night": "Erős havazás",
"77-day": "Hódara",
"77-night": "Snow Grains",
"77-night": "Hódara",
"80-day": "Enyhe záporok",
"80-night": "Light Showers",
"80-night": "Enyhe záporok",
"81-day": "Záporok",
"81-night": "Showers",
"81-night": "Záporok",
"82-day": "Heves záporok",
"82-night": "Heavy Showers",
"82-night": "Heves záporok",
"85-day": "Hózáporok",
"85-night": "Snow Showers",
"86-day": "Snow Showers",
"86-night": "Snow Showers",
"85-night": "Hózáporok",
"86-day": "Hózáporok",
"86-night": "Hózáporok",
"95-day": "Zivatar",
"95-night": "Thunderstorm",
"95-night": "Vihar",
"96-day": "Zivatar jégesővel",
"96-night": "Thunderstorm With Hail",
"99-day": "Thunderstorm With Hail",
"99-night": "Thunderstorm With Hail"
"96-night": "Zivatar jégesővel",
"99-day": "Zivatar jégesővel",
"99-night": "Zivatar jégesővel"
},
"homebridge": {
"available_update": "Rendszer",
@@ -530,17 +530,17 @@
"up_to_date": "Naprakész",
"child_bridges": "Gyerek Hidak",
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down"
"up": "Fut",
"pending": "Függőben lévő",
"down": "Leállt"
},
"healthchecks": {
"new": "Új",
"up": "Up",
"up": "Fut",
"grace": "Türelmi idő alatt",
"down": "Down",
"down": "Leállt",
"paused": "Szünetel",
"status": "Status",
"status": "Státusz",
"last_ping": "Legutóbbi Ping",
"never": "Még nincsenek ping-ek"
},
@@ -550,21 +550,21 @@
"containers_failed": "Sikertelen"
},
"autobrr": {
"approvedPushes": "Approved",
"approvedPushes": "Jóváhagyott",
"rejectedPushes": "Elutasított",
"filters": "Szűrők",
"indexers": "Indexers"
"indexers": "Indexerek"
},
"tubearchivist": {
"downloads": "Queue",
"downloads": "Várólista",
"videos": "Videók",
"channels": "Csatornák",
"playlists": "Lejátszási listák"
},
"truenas": {
"load": "Rendszerterhelés",
"uptime": "Uptime",
"alerts": "Alerts"
"uptime": "Működési idő",
"alerts": "Figyelmeztetések"
},
"pyload": {
"speed": "Sebesség",
@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "성공 중",
"num_failure_latest": "실패 중",
"bytes_added_30": "추가된 용량"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -955,7 +955,7 @@
"invalidConfiguration": "Invalid Configuration"
},
"frigate": {
"cameras": "Cameras",
"cameras": "Kamery",
"uptime": "Dostupnosť",
"version": "Verzia"
},
@@ -966,7 +966,7 @@
},
"zabbix": {
"unclassified": "Not classified",
"information": "Information",
"information": "Informácie",
"warning": "Warning",
"average": "Average",
"high": "High",
@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Успевајући",
"num_failure_latest": "Неуспешно",
"bytes_added_30": "Додати бајтови"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -14,20 +14,20 @@
"date": "{{value, date}}",
"relativeDate": "{{value, relativeDate}}",
"duration": "{{value, duration}}",
"months": "mo",
"days": "d",
"hours": "h",
"minutes": "m",
"seconds": "s"
"months": "tháng",
"days": "ngày",
"hours": "giờ",
"minutes": "phút",
"seconds": "giây"
},
"widget": {
"missing_type": "Thiếu loại Widget: {{type}}",
"api_error": "Lỗi API",
"information": "Information",
"information": "Thông tin",
"status": "Trạng thái",
"url": "URL",
"raw_error": "Raw Error",
"response_data": "Response Data"
"raw_error": "Lỗi thô",
"response_data": "Dữ liệu phản hồi"
},
"weather": {
"current": "Vị trí hiện tại",
@@ -44,106 +44,106 @@
"total": "Tổng",
"free": "Dư",
"used": "Đã dùng",
"load": "Load",
"load": "\n",
"temp": "TEMP",
"max": "Max",
"max": "Tối đa",
"uptime": "UP"
},
"unifi": {
"users": "Users",
"uptime": "Uptime",
"days": "Days",
"users": "Người dùng",
"uptime": "Thời gian hoạt động",
"days": "Ngày",
"wan": "WAN",
"lan": "LAN",
"wlan": "WLAN",
"devices": "Devices",
"lan_devices": "LAN Devices",
"wlan_devices": "WLAN Devices",
"lan_users": "LAN Users",
"wlan_users": "WLAN Users",
"devices": "Thiết bị",
"lan_devices": "Thiết bị trong mạng LAN",
"wlan_devices": "Thiết bị trong mạng WLAN",
"lan_users": "Người dùng mạng LAN",
"wlan_users": "Người dùng mạng WLAN",
"up": "UP",
"down": "DOWN",
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
"wait": "Vui lòng chờ",
"empty_data": "Trạng thái hệ thống phụ không xác định"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "MEM",
"mem": "Bộ nhớ",
"cpu": "CPU",
"running": "Running",
"running": "Đang hoạt động",
"offline": "Ngoại tuyến",
"error": "Error",
"unknown": "Unknown",
"healthy": "Healthy",
"starting": "Starting",
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"error": "Lỗi",
"unknown": "Không xác định",
"healthy": "Ổn định",
"starting": "Đang bắt đầu",
"unhealthy": "Bất thường",
"not_found": "Không tìm thấy",
"exited": "Exited",
"partial": "Partial"
},
"ping": {
"error": "Error",
"error": "Lỗi",
"ping": "Ping",
"down": "Down",
"up": "Up",
"not_available": "Không khả dụng"
},
"siteMonitor": {
"http_status": "HTTP status",
"error": "Error",
"response": "Response",
"http_status": "Trạng thái HTTP",
"error": "Lỗi",
"response": "Phản hồi",
"down": "Down",
"up": "Up",
"not_available": "Not Available"
"not_available": "Không có sẵn"
},
"emby": {
"playing": "Đang chơi",
"transcoding": "Chuyển định dạng",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"movies": "Movies",
"movies": "Phim ảnh",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs"
"songs": "Bài hát"
},
"esphome": {
"offline": "Offline",
"offline_alt": "Offline",
"online": "Online",
"total": "Total",
"unknown": "Unknown"
"unknown": "Không xác định"
},
"evcc": {
"pv_power": "Production",
"battery_soc": "Battery",
"grid_power": "Grid",
"battery_soc": "Pin",
"grid_power": "Lưới",
"home_power": "Consumption",
"charge_power": "Charger",
"charge_power": "Bộ sạc",
"kilowatt": "kW"
},
"flood": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"download": "Tải xuống",
"upload": "Tải lên",
"leech": "",
"seed": "Seed"
},
"freshrss": {
"subscriptions": "Subscriptions",
"unread": "Unread"
"subscriptions": "Đăng ký",
"unread": "Chưa đọc"
},
"fritzbox": {
"connectionStatus": "Status",
"connectionStatusUnconfigured": "Unconfigured",
"connectionStatusConnecting": "Connecting",
"connectionStatusAuthenticating": "Authenticating",
"connectionStatusPendingDisconnect": "Pending Disconnect",
"connectionStatusDisconnecting": "Disconnecting",
"connectionStatusDisconnected": "Disconnected",
"connectionStatusConnected": "Connected",
"uptime": "Uptime",
"maxDown": "Max. Down",
"maxUp": "Max. Up",
"connectionStatus": "Trạng thái",
"connectionStatusUnconfigured": "Chưa được cấu hình",
"connectionStatusConnecting": "Đang kết nối",
"connectionStatusAuthenticating": "Đang uỷ quyền",
"connectionStatusPendingDisconnect": "Đang chờ ngắt kết nối",
"connectionStatusDisconnecting": "Đang ngắt kết nối",
"connectionStatusDisconnected": "Đã ngắt kết nối",
"connectionStatusConnected": "Đã kết nối",
"uptime": "Thời gian hoạt động",
"maxDown": "Tải xuống tối đa",
"maxUp": "Tải lên tối đa",
"down": "Down",
"up": "Up",
"received": "Received",
@@ -999,64 +999,64 @@
"systems": "Systems",
"up": "Up",
"down": "Down",
"paused": "Paused",
"pending": "Pending",
"status": "Status",
"updated": "Updated",
"paused": "Đã tạm dừng",
"pending": "Đang xử lý",
"status": "Trạng thái",
"updated": "Đã cập nhật",
"cpu": "CPU",
"memory": "MEM",
"disk": "Disk",
"disk": "Ổ đĩa",
"network": "NET"
},
"argocd": {
"apps": "Apps",
"apps": "Ứng dụng",
"synced": "Synced",
"outOfSync": "Out Of Sync",
"healthy": "Healthy",
"healthy": "Ổn định",
"degraded": "Degraded",
"progressing": "Progressing",
"missing": "Missing",
"missing": "Bị thiếu",
"suspended": "Suspended"
},
"spoolman": {
"loading": "Loading"
"loading": "Đang tải"
},
"gitlab": {
"groups": "Groups",
"issues": "Issues",
"merges": "Merge Requests",
"projects": "Projects"
"groups": "Nhóm",
"issues": "Vấn đề",
"merges": "Yêu cầu Hợp nhất",
"projects": "Dự án"
},
"apcups": {
"status": "Status",
"load": "Load",
"bcharge": "Battery Charge",
"timeleft": "Time Left"
"status": "Trạng thái",
"load": "Đang tải\n",
"bcharge": "Sạc pin",
"timeleft": "Thời gian còn lại"
},
"karakeep": {
"bookmarks": "Bookmarks",
"favorites": "Favorites",
"archived": "Archived",
"highlights": "Highlights",
"lists": "Lists",
"tags": "Tags"
"bookmarks": "Dấu trang",
"favorites": "Mục yêu thích",
"archived": "Đã lưu trữ",
"highlights": "Tâm điểm",
"lists": "Danh sách",
"tags": "Thẻ"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
"slskStatus": "Mạng",
"connected": "Đã kết nối",
"disconnected": "Mất kết nối",
"updateStatus": "Cập nhật",
"update_yes": "Khả dụng",
"update_no": "Đã cập nhật",
"downloads": "Tải xuống",
"uploads": "Tải lên",
"sharedFiles": "Tập tin"
},
"jellystat": {
"songs": "Songs",
"movies": "Movies",
"episodes": "Episodes",
"other": "Other"
"songs": "Bài hát",
"movies": "Phim ảnh",
"episodes": "Tập",
"other": "Khác"
},
"checkmk": {
"serviceErrors": "Service issues",
@@ -1067,8 +1067,8 @@
"running": "Running",
"stopped": "Stopped",
"down": "Down",
"unhealthy": "Unhealthy",
"unknown": "Unknown",
"unhealthy": "Không ổn định",
"unknown": "Không xác định",
"servers": "Servers",
"stacks": "Stacks",
"containers": "Containers"
@@ -1076,18 +1076,18 @@
"filebrowser": {
"available": "Available",
"used": "Used",
"total": "Total"
"total": "Tổng"
},
"wallos": {
"activeSubscriptions": "Subscriptions",
"thisMonthlyCost": "This Month",
"nextMonthlyCost": "Next Month",
"previousMonthlyCost": "Prev. Month",
"nextRenewingSubscription": "Next Payment"
"activeSubscriptions": "Đăng ký",
"thisMonthlyCost": "Tháng này",
"nextMonthlyCost": "Tháng sau",
"previousMonthlyCost": "Tháng trước",
"nextRenewingSubscription": "Lần thanh toán kế tiếp"
},
"unraid": {
"STARTED": "Started",
"STOPPED": "Stopped",
"STARTED": "Đã bắt đầu",
"STOPPED": "Đã dừng",
"NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",
@@ -1096,9 +1096,9 @@
"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",
"status": "Status",
"NO_DATA_DISKS": "Không có dữ liệu ổ đĩa",
"notifications": "Thông báo",
"status": "Trạng thái",
"cpu": "CPU",
"memoryUsed": "Memory Used",
"memoryAvailable": "Memory Available",
@@ -1108,11 +1108,16 @@
"poolFree": "{{pool}} Free"
},
"backrest": {
"num_plans": "Plans",
"num_success_30": "Successes",
"num_failure_30": "Failures",
"num_plans": "Các kế hoạch",
"num_success_30": "Thành công",
"num_failure_30": "Thất bại",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Bài hát",
"time": "Thời gian",
"artists": "Nghệ sĩ"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1114,5 +1114,10 @@
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -1,47 +1,16 @@
import classNames from "classnames";
import { useTranslation } from "next-i18next";
import { useContext, useMemo } from "react";
import { BlockHighlightContext } from "./highlight-context";
import { evaluateHighlight, getHighlightClass } from "utils/highlights";
export default function Block({ value, label, field }) {
export default function Block({ value, label }) {
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,10 +1,7 @@
import { useContext, useMemo } from "react";
import { useContext } 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",
@@ -14,11 +11,6 @@ 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;
@@ -59,11 +51,6 @@ export default function Container({ error = false, children, service }) {
}),
);
}
const content = <div className="relative flex flex-row w-full service-container">{visibleChildren}</div>;
if (!highlightConfig) {
return content;
}
return <BlockHighlightContext.Provider value={highlightConfig}>{content}</BlockHighlightContext.Provider>;
return <div className="relative flex flex-row w-full service-container">{visibleChildren}</div>;
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
export default function QueueEntry({ title, activity, timeLeft, progress, size }) {
export default function QueueEntry({ title, activity, timeLeft, progress }) {
return (
<div className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex">
<div
@@ -11,7 +11,6 @@ export default function QueueEntry({ title, activity, timeLeft, progress, size }
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden text-left">{title}</div>
</div>
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
{size && `${size} - `}
{timeLeft ? `${activity} - ${timeLeft}` : activity}
</div>
</div>

View File

@@ -417,7 +417,6 @@ function Home({ initialSettings }) {
)}
<meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
<meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
<meta name="color-scheme" content="dark light"></meta>
</Head>
<Script src="/api/config/custom.js" />
@@ -540,48 +539,38 @@ export default function Wrapper({ initialSettings, fallback }) {
html.classList.add(desiredThemeClass);
}
if (backgroundImage) {
const safeBackgroundImage = backgroundImage.replace(/'/g, "\\'");
body.style.backgroundImage = `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${safeBackgroundImage}')`;
body.style.backgroundSize = "cover";
body.style.backgroundPosition = "center";
body.style.backgroundAttachment = "fixed";
body.style.backgroundRepeat = "no-repeat";
body.style.backgroundColor = "";
} else {
body.style.backgroundImage = "none";
body.style.backgroundColor = "rgb(var(--bg-color))";
body.style.backgroundSize = "";
body.style.backgroundPosition = "";
body.style.backgroundAttachment = "";
body.style.backgroundRepeat = "";
}
return () => {
body.style.backgroundImage = "";
body.style.backgroundColor = "";
body.style.backgroundSize = "";
body.style.backgroundPosition = "";
body.style.backgroundAttachment = "";
body.style.backgroundRepeat = "";
};
// Remove any previously applied inline styles
body.style.backgroundImage = "";
body.style.backgroundColor = "";
body.style.backgroundAttachment = "";
}, [backgroundImage, opacity, theme, color, initialSettings.color]);
return (
<div id="page_wrapper" className="relative min-h-screen">
<div
id="inner_wrapper"
tabIndex="-1"
className={classNames(
"w-full min-h-screen overflow-auto",
backgroundBlur &&
`backdrop-blur${initialSettings.background.blur?.length ? `-${initialSettings.background.blur}` : ""}`,
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
)}
>
<Index initialSettings={initialSettings} fallback={fallback} />
<>
{backgroundImage && (
<div
id="background"
aria-hidden="true"
style={{
backgroundImage: `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${backgroundImage}')`,
}}
/>
)}
<div id="page_wrapper" className="relative h-full">
<div
id="inner_wrapper"
tabIndex="-1"
className={classNames(
"w-full h-full overflow-auto",
backgroundBlur &&
`backdrop-blur${initialSettings.background.blur?.length ? `-${initialSettings.background.blur}` : ""}`,
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
)}
>
<Index initialSettings={initialSettings} fallback={fallback} />
</div>
</div>
</div>
</>
);
}

View File

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

View File

@@ -254,7 +254,6 @@ export function cleanServiceGroups(groups) {
// all widgets
fields,
hideErrors,
highlight,
type,
// azuredevops
@@ -285,7 +284,6 @@ export function cleanServiceGroups(groups) {
// deluge, qbittorrent
enableLeechProgress,
enableLeechSize,
// diskstation
volume,
@@ -442,21 +440,6 @@ 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;
@@ -510,7 +493,6 @@ export function cleanServiceGroups(groups) {
}
if (["deluge", "qbittorrent"].includes(type)) {
if (enableLeechProgress !== undefined) widget.enableLeechProgress = JSON.parse(enableLeechProgress);
if (enableLeechSize !== undefined) widget.enableLeechSize = JSON.parse(enableLeechSize);
}
if (["opnsense", "pfsense"].includes(type)) {
if (wan) widget.wan = wan;

View File

@@ -1,257 +0,0 @@
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,11 +24,10 @@ 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 if (statuses[0] === "STATUS_ERROR") {
} else {
numFailureLatest++;
}
}

View File

@@ -205,14 +205,13 @@ 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, enableNowPlaying ? "Sessions" : "", {
refreshInterval: enableNowPlaying ? 5000 : undefined,
} = useWidgetAPI(widget, "Sessions", {
refreshInterval: 5000,
});
const { data: countData, error: countError } = useWidgetAPI(widget, "Count", {
@@ -240,12 +239,13 @@ 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 ((enableNowPlaying && !sessionsData) || !countData) {
if (!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.available }];
const newDataPoints = [...prevDataPoints, { a: data.used, b: data.free }];
if (newDataPoints.length > pointsLimit) {
newDataPoints.shift();
}
@@ -67,10 +67,10 @@ export default function Component({ service }) {
{data && !error && (
<Block position="bottom-3 left-3">
{data.available && chart && (
{data.free && chart && (
<div className="text-xs opacity-50">
{t("common.bytes", {
value: data.available,
value: data.free,
maximumFractionDigits: 1,
binary: true,
})}{" "}
@@ -93,10 +93,10 @@ export default function Component({ service }) {
{!chart && (
<Block position="top-3 right-3">
{data.available && (
{data.free && (
<div className="text-xs opacity-50">
{t("common.bytes", {
value: data.available,
value: data.free,
maximumFractionDigits: 1,
binary: true,
})}{" "}

View File

@@ -33,6 +33,14 @@ 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", {
@@ -47,14 +55,6 @@ 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, 6].includes(controllerVersionMajor)) {
if (![3, 4, 5].includes(controllerVersionMajor)) {
return res.status(500).json({ error: { message: "Error determining controller version", data } });
}
@@ -80,7 +80,6 @@ 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:
@@ -123,7 +122,6 @@ 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:
@@ -209,8 +207,8 @@ export default async function omadaProxyHandler(req, res) {
connectedAp = siteResponseData.result.connectedAp;
activeUser = siteResponseData.result.activeUser;
alerts = siteResponseData.result.alerts;
} else if ([4, 5, 6].includes(controllerVersionMajor)) {
const siteName = controllerVersionMajor > 4 ? site.id : site.key;
} else if (controllerVersionMajor === 4 || controllerVersionMajor === 5) {
const siteName = controllerVersionMajor === 5 ? site.id : site.key;
const siteStatsUrl =
controllerVersionMajor === 4
? `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`

View File

@@ -80,11 +80,6 @@ export default function Component({ service }) {
timeLeft={t("common.duration", { value: queueEntry.eta })}
title={queueEntry.name}
activity={queueEntry.state}
size={
widget?.enableLeechSize
? t("common.bbytes", { value: queueEntry.size, maximumFractionDigits: 1 })
: undefined
}
key={`${queueEntry.name}-${queueEntry.amount_left}`}
/>
))}

View File

@@ -52,7 +52,6 @@ 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:
@@ -63,7 +62,7 @@ export default function Component({ service }) {
break;
case 2:
status = t("uptimerobot.up");
uptime = t("common.duration", { value: hasLogs ? monitor.logs[0].duration : 0 });
uptime = t("common.duration", { value: monitor.logs[0].duration });
logIndex = 1;
break;
case 8:
@@ -77,14 +76,14 @@ export default function Component({ service }) {
break;
}
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);
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;
return (
<Container service={service}>
<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.downDuration" value={downDuration} />}
</Container>