Compare commits

..

7 Commits

Author SHA1 Message Date
shamoon
02989a4366 Bump version to 1.12.0 2026-03-27 15:18:07 -07:00
shamoon
bc6acf7fd1 Merge branch 'dev' 2026-03-27 15:17:33 -07:00
shamoon
a4e29bc7a7 1.11.0 2026-03-14 08:58:53 -07:00
shamoon
a7982bda06 Merge branch 'dev' 2026-03-14 08:58:38 -07:00
shamoon
6b3bff1f1d Fix typo in shortcuts documentation 2026-03-07 16:13:08 -08:00
shamoon
e3ca0adf11 Documentation: add 'unit' option for temperature in glances config 2026-02-20 22:12:12 -08:00
Kristiyan Nikolov
d62404f164 Documentation: Fix doc heading for PWA/App icons (#6290) 2026-02-05 11:36:19 -08:00
68 changed files with 214 additions and 744 deletions

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v6
- name: crowdin action
uses: crowdin/github-action@7ca9c452bfe9197d3bb7fa83a4d7e2b0c9ae835d # v2
uses: crowdin/github-action@v2
with:
upload_translations: false
download_translations: true

View File

@@ -17,12 +17,44 @@ env:
IMAGE_NAME: ${{ github.repository }}
jobs:
pre-commit:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@v5
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint frontend
run: pnpm run lint
build:
name: Docker Build & Push
if: github.repository == 'gethomepage/homepage'
runs-on: ubuntu-22.04
needs: [ pre-commit ]
permissions:
contents: read
packages: write
@@ -30,11 +62,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v6
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@v6
with:
images: |
${{ env.IMAGE_NAME }}
@@ -52,7 +84,7 @@ jobs:
latest=auto
- name: Next.js build cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@v5
with:
path: .next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx') }}
@@ -60,13 +92,13 @@ jobs:
nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
uses: pnpm/action-setup@v5
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
@@ -83,7 +115,7 @@ jobs:
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -91,20 +123,20 @@ jobs:
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@v4.0.0
- name: Setup Docker buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@v4
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
uses: docker/build-push-action@v7
with:
context: .
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -14,18 +14,32 @@ permissions:
id-token: write
jobs:
pre-commit:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@v3.0.1
test:
name: Test Build Docs
if: github.repository == 'gethomepage/homepage' && github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs:
- pre-commit
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
uses: astral-sh/setup-uv@v7
- run: sudo apt-get install pngquant
- name: Test Docs Build
run: uv run --frozen zensical build --clean
@@ -36,19 +50,21 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs:
- pre-commit
steps:
- uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- uses: actions/configure-pages@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
uses: astral-sh/setup-uv@v7
- run: sudo apt-get install pngquant
- name: Build Docs
run: uv run --frozen zensical build --clean
- uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
- uses: actions/upload-pages-artifact@v4
with:
path: site
- uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
- uses: actions/deploy-pages@v4
id: deployment

View File

@@ -1,41 +0,0 @@
name: Lint
on:
pull_request:
push:
workflow_dispatch:
merge_group:
jobs:
lint:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint frontend
run: pnpm run lint

View File

@@ -13,6 +13,6 @@ jobs:
anti-slop:
runs-on: ubuntu-latest
steps:
- uses: peakoss/anti-slop@a5a4b2440c9de6f65b64f0718a0136a1fdb04f6f # v0
- uses: peakoss/anti-slop@v0
with:
max-failures: 4

View File

@@ -15,4 +15,4 @@ jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/reaction-comments@e86d247c12bd5c043eec379a1a4453f20cadf913 # v4
- uses: dessant/reaction-comments@v4

View File

@@ -26,14 +26,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != ''
uses: release-drafter/release-drafter@a6acf82562eee06318b77ab8cb0b11ed81c677a7 # v7
uses: release-drafter/release-drafter@v7
with:
config-name: release-drafter.yml
version: ${{ github.event.inputs.version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == ''
uses: release-drafter/release-drafter@a6acf82562eee06318b77ab8cb0b11ed81c677a7 # v7
uses: release-drafter/release-drafter@v7
with:
config-name: release-drafter.yml
env:

View File

@@ -18,7 +18,7 @@ jobs:
name: 'Stale'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
- uses: actions/stale@v10
with:
days-before-stale: 7
days-before-close: 14
@@ -32,7 +32,7 @@ jobs:
name: 'Lock Old Threads'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6
- uses: dessant/lock-threads@v6
with:
issue-inactive-days: '30'
pr-inactive-days: '30'
@@ -57,7 +57,7 @@ jobs:
name: 'Close Answered Discussions'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {
@@ -113,7 +113,7 @@ jobs:
name: 'Close Outdated Discussions'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {
@@ -204,7 +204,7 @@ jobs:
name: 'Close Unsupported Feature Requests'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {

View File

@@ -13,13 +13,13 @@ jobs:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@v6
- uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
- uses: pnpm/action-setup@v5
with:
version: 9
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: pnpm
@@ -28,7 +28,7 @@ jobs:
# Run Vitest directly so `--shard` is parsed as an option
- run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info

View File

@@ -129,7 +129,7 @@ A progressive web app is an app that can be installed on a device and provide us
More information on PWAs can be found in [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps).
## App icons
### App icons
You can set custom icons for installable apps. More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons).
@@ -150,7 +150,7 @@ For icon `src` you can pass either full URL or a local path relative to the `/ap
### Shortcuts
Shortcuts can e used to specify links to tabs, to be preselected when the homepage is opened as an app.
Shortcuts can be used to specify links to tabs, to be preselected when the homepage is opened as an app.
More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/shortcuts).
```yaml

View File

@@ -16,6 +16,7 @@ The Glances widget allows you to monitor the resources (CPU, memory, storage, te
cpu: true # optional, enabled by default, disable by setting to false
mem: true # optional, enabled by default, disable by setting to false
cputemp: true # disabled by default
unit: imperial # optional for temp, default is metric
uptime: true # disabled by default
disk: / # disabled by default, use mount point of disk(s) in glances. Can also be a list (see below)
diskUnits: bytes # optional, bytes (default) or bbytes. Only applies to disk
@@ -31,5 +32,3 @@ disk:
- /boot
...
```
_Added in v0.4.18, updated in v0.6.11, v0.6.21_

View File

@@ -1,24 +0,0 @@
---
title: UniFi Drive
description: UniFi Drive Widget Configuration
---
Learn more about [UniFi Drive](https://ui.com/integrations/network-storage).
## Configuration
Displays storage statistics from your UniFi Network Attached Storage (UNAS) device. Requires a local UniFi account with at least read privileges.
Allowed fields: `["total", "used", "available", "status"]`
```yaml
widget:
type: unifi_drive
url: https://unifi.host.or.ip
username: your_username
password: your_password
```
!!! hint
If you enter incorrect credentials and receive an "API Error", you may need to recreate the container or restart the service to clear the cache.

View File

@@ -171,7 +171,6 @@ nav:
- widgets/services/truenas.md
- widgets/services/tubearchivist.md
- widgets/services/unifi-controller.md
- widgets/services/unifi-drive.md
- widgets/services/unmanic.md
- widgets/services/unraid.md
- widgets/services/uptime-kuma.md

View File

@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.10.1",
"version": "1.12.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

16
pnpm-lock.yaml generated
View File

@@ -1593,11 +1593,11 @@ packages:
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
brace-expansion@1.1.13:
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@2.0.3:
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -5162,12 +5162,12 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
brace-expansion@1.1.13:
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.0.3:
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
@@ -6598,11 +6598,11 @@ snapshots:
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.13
brace-expansion: 1.1.12
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.3
brace-expansion: 2.0.2
minimist@1.2.8: {}

View File

@@ -66,11 +66,6 @@
"wait": "Wag asseblief",
"empty_data": "Substelsel status onbekend"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "حالة النظام الفرعي غير معروفة"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "استقبال",
"tx": "ارسال",

View File

@@ -66,11 +66,6 @@
"wait": "Моля изчакайте",
"empty_data": "Неизвестен статус на подсистема"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "ПЧ",
"tx": "ИЗ",

View File

@@ -66,11 +66,6 @@
"wait": "Si us plau espera",
"empty_data": "Estat del subsistema desconegut"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "Rebut",
"tx": "Transmès",

View File

@@ -39,7 +39,7 @@
"placeholder": "Hledat…"
},
"resources": {
"cpu": "Využití procesoru",
"cpu": "Zatížení procesoru",
"mem": "Využití paměti",
"total": "Celkem",
"free": "Volné",
@@ -66,16 +66,11 @@
"wait": "Čekejte prosím",
"empty_data": "Stav podsystému neznámý"
},
"unifi_drive": {
"healthy": "Zdravý",
"degraded": "Degradováno",
"no_data": "Nejsou k dispozici žádná data úložiště"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "Využití paměti",
"cpu": "Využití procesoru",
"cpu": "Zatížení procesoru",
"running": "Běží",
"offline": "Offline",
"error": "Chyba",
@@ -237,7 +232,7 @@
"seed": "Seedované"
},
"qnap": {
"cpuUsage": "Využití procesoru",
"cpuUsage": "Zatížení procesoru",
"memUsage": "Využití paměti",
"systemTempC": "Teplota systému",
"poolUsage": "Využití fondu",
@@ -450,12 +445,12 @@
},
"proxmox": {
"mem": "Využití paměti",
"cpu": "Využití procesoru",
"cpu": "Zatížení procesoru",
"lxc": "LXC",
"vms": "Virtuální Stroje"
},
"glances": {
"cpu": "Využití procesoru",
"cpu": "Zatížení procesoru",
"load": "Zatížení",
"wait": "Čekejte prosím",
"temp": "TEPLOTA",
@@ -640,7 +635,7 @@
"no_devices": "Žádná přijatá data zařízení"
},
"mikrotik": {
"cpuLoad": "Využití procesoru",
"cpuLoad": "Zatížení procesoru",
"memoryUsed": "Využití paměti",
"uptime": "Doba provozu",
"numberOfLeases": "Pronájmy"
@@ -691,7 +686,7 @@
"proxmoxbackupserver": {
"datastore_usage": "Datové úložiště",
"failed_tasks_24h": "Neúspěšné úlohy 24h",
"cpu_usage": "Využití procesoru",
"cpu_usage": "Zatížení procesoru",
"memory_usage": "Využití paměti"
},
"immich": {
@@ -755,7 +750,7 @@
"alertstriggered": "Spuštěné výstrahy"
},
"nextcloud": {
"cpuload": "Využití procesoru",
"cpuload": "Zatížení procesoru",
"memoryusage": "Využití paměti",
"freespace": "Volný prostor",
"activeusers": "Aktivní uživatelé",
@@ -878,7 +873,7 @@
},
"openwrt": {
"uptime": "Doba provozu",
"cpuLoad": "Prům. využití procesoru (5m)",
"cpuLoad": "Prům. zatížení procesoru (5m)",
"up": "Běží",
"down": "Výpadek",
"bytesTx": "Přeneseno",
@@ -1042,7 +1037,7 @@
"pending": "Čekající",
"status": "Stav",
"updated": "Aktualizováno",
"cpu": "Využití procesoru",
"cpu": "Zatížení procesoru",
"memory": "Využití paměti",
"disk": "Disk",
"network": "Síť"
@@ -1138,7 +1133,7 @@
"NO_DATA_DISKS": "Žádné datové disky",
"notifications": "Upozornění",
"status": "Stav",
"cpu": "Využití procesoru",
"cpu": "Zatížení procesoru",
"memoryUsed": "Využití paměti",
"memoryAvailable": "Volná paměť",
"arrayUsed": "Využito pole",
@@ -1169,7 +1164,7 @@
"dockhand": {
"running": "Běží",
"stopped": "Zastaveno",
"cpu": "Využití procesoru",
"cpu": "Zatížení procesoru",
"memory": "Využití paměti",
"images": "Obrazy",
"volumes": "Úložiště",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status ukendt"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Bitte warten",
"empty_data": "Subsystem-Status unbekannt"
},
"unifi_drive": {
"healthy": "Gesund",
"degraded": "Beeinträchtigt",
"no_data": "Keine Speicherdaten verfügbar"
},
"docker": {
"rx": "RX",
"tx": "TX",
@@ -120,7 +115,7 @@
"movies": "Filme",
"series": "Serien",
"episodes": "Episoden",
"songs": "Titel"
"songs": "Songs"
},
"esphome": {
"offline": "Offline",
@@ -190,10 +185,10 @@
"plex_connection_error": "Prüfe Plex-Verbindung"
},
"tracearr": {
"no_active": "Keine aktiven Streams",
"no_active": "No Active Streams",
"streams": "Streams",
"transcodes": "Transkodieren",
"directplay": "Direkte Wiedergabe",
"transcodes": "Transcodes",
"directplay": "Direct Play",
"bitrate": "Bitrate"
},
"omada": {
@@ -295,12 +290,12 @@
"available": "Verfügbar"
},
"seerr": {
"pending": "Ausstehend",
"approved": "Bestätigt",
"available": "Verfügbar",
"completed": "Abgeschlossen",
"processing": "Wird verarbeitet",
"issues": "Offene Probleme"
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"completed": "Completed",
"processing": "Processing",
"issues": "Open Issues"
},
"netalertx": {
"total": "Total",
@@ -620,7 +615,7 @@
},
"pangolin": {
"orgs": "Orgs",
"sites": "Seiten",
"sites": "Sites",
"resources": "Ressourcen",
"targets": "Ziele",
"traffic": "Traffic",
@@ -724,7 +719,7 @@
"volumeAvailable": "Verfügbar"
},
"dispatcharr": {
"channels": "Kanäle",
"channels": "Channels",
"streams": "Streams"
},
"mylar": {
@@ -816,10 +811,10 @@
"series": "Serien"
},
"booklore": {
"libraries": "Bibliotheken",
"libraries": "Libraries",
"books": "Bücher",
"reading": "Am Lesen",
"finished": "Fertig"
"reading": "Reading",
"finished": "Finished"
},
"jdownloader": {
"downloadCount": "Warteschlange",
@@ -1160,11 +1155,11 @@
"artists": "Künstler"
},
"arcane": {
"containers": "Container",
"containers": "Containers",
"images": "Images",
"image_updates": "Image-Updates",
"images_unused": "Ungenutzt",
"environment_required": "Umgebungs-ID erforderlich"
"image_updates": "Image Updates",
"images_unused": "Unused",
"environment_required": "Environment ID Required"
},
"dockhand": {
"running": "Wird ausgeführt",
@@ -1181,9 +1176,9 @@
"environment_not_found": "Umgebung nicht gefunden"
},
"sparkyfitness": {
"eaten": "",
"burned": "Verbrannt",
"remaining": "Verbleibend",
"steps": "Schritte"
"eaten": "Eaten",
"burned": "Burned",
"remaining": "Remaining",
"steps": "Steps"
}
}

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Άγνωστη κατάσταση υποσυστήματος"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsistemostatuso nekonata"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Espere, por favor",
"empty_data": "Se desconoce el estado del subsistema"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "Recibido",
"tx": "Transmitido",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Veuillez patienter",
"empty_data": "Statut du sous-système inconnu"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "Rx",
"tx": "Tx",

View File

@@ -66,11 +66,6 @@
"wait": "נא להמתין",
"empty_data": "מצב תת-מערכת לא ידוע"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Pričekaj",
"empty_data": "Stanje podsustava nepoznato"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Kérjük várjon",
"empty_data": "Az alrendszer állapota ismeretlen"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Status subsistem tdk diketahui"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Stato del sottosistema sconosciuto"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "お待ちください",
"empty_data": "サブシステムの状態は不明"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "受信済み",
"tx": "送信済み",

View File

@@ -66,11 +66,6 @@
"wait": "잠시만 기다려주세요",
"empty_data": "서브시스템 상태 알 수 없음"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "수신",
"tx": "송신",
@@ -113,14 +108,14 @@
"songs": "음악"
},
"jellyfin": {
"playing": "재생 중",
"transcoding": "트랜스코딩 중",
"bitrate": "비트레이트",
"no_active": "활성 스트림 없음",
"movies": "영상",
"series": "시리즈",
"episodes": "에피소드",
"songs": "음악"
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs"
},
"esphome": {
"offline": "오프라인",
@@ -190,11 +185,11 @@
"plex_connection_error": "Plex 연결 확인"
},
"tracearr": {
"no_active": "활성 스트림 없음",
"streams": "스트림",
"transcodes": "트랜스코드",
"directplay": "다이렉트 플레이",
"bitrate": "비트레이트"
"no_active": "No Active Streams",
"streams": "Streams",
"transcodes": "Transcodes",
"directplay": "Direct Play",
"bitrate": "Bitrate"
},
"omada": {
"connectedAp": "연결된 AP",
@@ -295,12 +290,12 @@
"available": "이용 가능"
},
"seerr": {
"pending": "대기 중",
"approved": "승인됨",
"available": "사용 가능",
"completed": "완료됨",
"processing": "처리 중",
"issues": "열린 이슈"
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"completed": "Completed",
"processing": "Processing",
"issues": "Open Issues"
},
"netalertx": {
"total": "전체",
@@ -551,7 +546,7 @@
"up": "업",
"pending": "대기 중",
"down": "다운",
"ok": "확인"
"ok": "Ok"
},
"healthchecks": {
"new": "신규",
@@ -623,9 +618,9 @@
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
"traffic": "트래픽",
"in": "수신",
"out": "송신"
"traffic": "Traffic",
"in": "In",
"out": "Out"
},
"peanut": {
"battery_charge": "배터리 충전",
@@ -724,8 +719,8 @@
"volumeAvailable": "사용 가능"
},
"dispatcharr": {
"channels": "채널",
"streams": "스트림"
"channels": "Channels",
"streams": "Streams"
},
"mylar": {
"series": "시리즈",
@@ -792,7 +787,7 @@
"gross_percent_today": "오늘",
"gross_percent_1y": "1년",
"gross_percent_max": "전체 기간",
"net_worth": "순자산"
"net_worth": "Net Worth"
},
"audiobookshelf": {
"podcasts": "팟캐스트",
@@ -816,10 +811,10 @@
"series": "시리즈"
},
"booklore": {
"libraries": "라이브러리",
"books": "",
"reading": "읽는 중",
"finished": "완료"
"libraries": "Libraries",
"books": "Books",
"reading": "Reading",
"finished": "Finished"
},
"jdownloader": {
"downloadCount": "대기열",
@@ -1155,30 +1150,30 @@
"bytes_added_30": "추가된 용량"
},
"yourspotify": {
"songs": "음악",
"time": "시간",
"artists": "아티스트"
"songs": "Songs",
"time": "Time",
"artists": "Artists"
},
"arcane": {
"containers": "컨테이너",
"images": "이미지",
"image_updates": "이미지 업데이트",
"images_unused": "미사용",
"environment_required": "환경 ID 필요"
"containers": "Containers",
"images": "Images",
"image_updates": "Image Updates",
"images_unused": "Unused",
"environment_required": "Environment ID Required"
},
"dockhand": {
"running": "실행 중",
"stopped": "정지됨",
"running": "Running",
"stopped": "Stopped",
"cpu": "CPU",
"memory": "메모리",
"images": "이미지",
"volumes": "볼륨",
"events_today": "오늘의 이벤트",
"pending_updates": "대기 중인 업데이트",
"stacks": "스택",
"paused": "일시정지됨",
"total": "전체",
"environment_not_found": "환경 없음"
"memory": "Memory",
"images": "Images",
"volumes": "Volumes",
"events_today": "Events Today",
"pending_updates": "Pending Updates",
"stacks": "Stacks",
"paused": "Paused",
"total": "Total",
"environment_not_found": "Environment Not Found"
},
"sparkyfitness": {
"eaten": "Eaten",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Status subsistem tak diketahui"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Even geduld",
"empty_data": "Subsysteem status onbekend"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Ukjent undersystemstatus"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Proszę czekać",
"empty_data": "Status podsystemu nieznany"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "Rx",
"tx": "Tx",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Status de Subsistema Desconhecido"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "Rx",
"tx": "Tx",

View File

@@ -14,7 +14,7 @@
"date": "{{value, date}}",
"relativeDate": "{{value, relativeDate}}",
"duration": "{{value, duration}}",
"months": "mo",
"months": "M",
"days": "d",
"hours": "h",
"minutes": "m",
@@ -66,14 +66,9 @@
"wait": "Por favor, aguarde",
"empty_data": "Status do Subsistema desconhecido"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",
"rx": "Rx",
"tx": "Tx",
"mem": "MEM",
"cpu": "CPU",
"running": "Executando",
@@ -106,21 +101,21 @@
"playing": "A reproduzir",
"transcoding": "Transcodificação",
"bitrate": "Taxa de bits",
"no_active": "Sem Transmissões Ativas",
"no_active": "Sem Streams Ativos",
"movies": "Filmes",
"series": "Séries",
"episodes": "Episódios",
"songs": "Canções"
},
"jellyfin": {
"playing": "Jogando",
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"movies": "Filmes",
"series": "Séries",
"episodes": "Episódios",
"songs": "Músicas"
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs"
},
"esphome": {
"offline": "Offline",
@@ -295,12 +290,12 @@
"available": "Disponível"
},
"seerr": {
"pending": "Pendente",
"approved": "Aprovado",
"available": "Disponível",
"completed": "Concluído",
"processing": "Processando",
"issues": "Erros pendentes"
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"completed": "Completed",
"processing": "Processing",
"issues": "Open Issues"
},
"netalertx": {
"total": "Total",
@@ -621,7 +616,7 @@
"pangolin": {
"orgs": "Orgs",
"sites": "Sites",
"resources": "Recursos",
"resources": "Resources",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
@@ -724,8 +719,8 @@
"volumeAvailable": "Disponível"
},
"dispatcharr": {
"channels": "Canais",
"streams": "Transmissões"
"channels": "Channels",
"streams": "Streams"
},
"mylar": {
"series": "Séries",
@@ -816,10 +811,10 @@
"series": "Séries"
},
"booklore": {
"libraries": "Bibliotecas",
"books": "Livros",
"reading": "Lendo",
"finished": "Finalizado"
"libraries": "Libraries",
"books": "Books",
"reading": "Reading",
"finished": "Finished"
},
"jdownloader": {
"downloadCount": "Fila de espera",
@@ -1160,23 +1155,23 @@
"artists": "Artistas"
},
"arcane": {
"containers": "Recipientes",
"images": "Imagens",
"image_updates": "Atualizações de Imagem",
"images_unused": "Não utilizado",
"containers": "Containers",
"images": "Images",
"image_updates": "Image Updates",
"images_unused": "Unused",
"environment_required": "Environment ID Required"
},
"dockhand": {
"running": "Executando",
"running": "Running",
"stopped": "Stopped",
"cpu": "CPU",
"memory": "Memória",
"images": "Imagens",
"volumes": "Quantidades",
"events_today": "Eventos hoje",
"pending_updates": "Atualizações pendentes",
"stacks": "Pilhas",
"paused": "Pausado",
"memory": "Memory",
"images": "Images",
"volumes": "Volumes",
"events_today": "Events Today",
"pending_updates": "Pending Updates",
"stacks": "Stacks",
"paused": "Paused",
"total": "Total",
"environment_not_found": "Environment Not Found"
},

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Starea subsistemului este necunoscut"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Пожалуйста, подождите",
"empty_data": "Статус подсистемы неизвестен"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Čakajte, prosím",
"empty_data": "Stav podsystému neznámy"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "Prijaté",
"tx": "Odoslané",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Neznani status podsistema"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Молим сачекајте",
"empty_data": "Статус подсистема непознат"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "Subsystem status unknown"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Lütfen bekleyin",
"empty_data": "Alt sistem durumu bilinmiyor"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "Gelen Veri",
"tx": "Giden Veri",

View File

@@ -66,11 +66,6 @@
"wait": "Будь ласка, зачекайте",
"empty_data": "Статус підсистеми невідомий"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Vui lòng chờ",
"empty_data": "Trạng thái hệ thống phụ không xác định"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "RX",
"tx": "TX",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "子系統狀態未知"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "接收",
"tx": "發送",

View File

@@ -66,11 +66,6 @@
"wait": "请稍候",
"empty_data": "子系统状态未知"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "接收",
"tx": "发送",

View File

@@ -66,11 +66,6 @@
"wait": "Please wait",
"empty_data": "子系統狀態未知"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": {
"rx": "接收",
"tx": "傳送",

View File

@@ -147,7 +147,6 @@ const components = {
tubearchivist: dynamic(() => import("./tubearchivist/component")),
truenas: dynamic(() => import("./truenas/component")),
unifi: dynamic(() => import("./unifi/component")),
unifi_drive: dynamic(() => import("./unifi_drive/component")),
unmanic: dynamic(() => import("./unmanic/component")),
unraid: dynamic(() => import("./unraid/component")),
uptimekuma: dynamic(() => import("./uptimekuma/component")),

View File

@@ -12,7 +12,7 @@ async function login(widget) {
const loginParams = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
body: null,
};
if (widget.username && widget.password) {

View File

@@ -45,7 +45,7 @@ describe("widgets/flood/proxy", () => {
expect(httpProxy).toHaveBeenCalledTimes(3);
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://flood/api/stats");
expect(httpProxy.mock.calls[1][0]).toBe("http://flood/api/auth/authenticate");
expect(httpProxy.mock.calls[1][1].body).toBe("{}");
expect(httpProxy.mock.calls[1][1].body).toBeNull();
expect(httpProxy.mock.calls[2][0].toString()).toBe("http://flood/api/stats");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("data"));

View File

@@ -1,58 +0,0 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: storageData, error: storageError } = useWidgetAPI(widget, "storage");
if (storageError) {
return <Container service={service} error={storageError} />;
}
if (!storageData) {
return (
<Container service={service}>
<Block field="unifi_drive.total" label="resources.total" />
<Block field="unifi_drive.used" label="resources.used" />
<Block field="unifi_drive.available" label="resources.free" />
<Block field="unifi_drive.status" label="widget.status" />
</Container>
);
}
const { data: storage } = storageData;
if (!storage) {
return (
<Container service={service}>
<Block value={t("unifi_drive.no_data")} />
</Container>
);
}
const { totalQuota, usage, status } = storage;
const totalBytes = totalQuota ?? 0;
const usedBytes = (usage?.system || 0) + (usage?.myDrives || 0) + (usage?.sharedDrives || 0);
const availableBytes = Math.max(0, totalBytes - usedBytes);
let statusValue = status;
if (status === "healthy") statusValue = t("unifi_drive.healthy");
else if (status === "degraded") statusValue = t("unifi_drive.degraded");
return (
<Container service={service}>
<Block field="unifi_drive.total" label="resources.total" value={t("common.bytes", { value: totalBytes })} />
<Block field="unifi_drive.used" label="resources.used" value={t("common.bytes", { value: usedBytes })} />
<Block
field="unifi_drive.available"
label="resources.free"
value={t("common.bytes", { value: availableBytes })}
/>
<Block field="unifi_drive.status" label="widget.status" value={statusValue} />
</Container>
);
}

View File

@@ -1,92 +0,0 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
import { expectBlockValue } from "test-utils/widget-assertions";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Component from "./component";
describe("widgets/unifi_drive/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("resources.total")).toBeInTheDocument();
expect(screen.getByText("resources.used")).toBeInTheDocument();
expect(screen.getByText("resources.free")).toBeInTheDocument();
expect(screen.getByText("widget.status")).toBeInTheDocument();
});
it("renders error when API fails", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: new Error("fail") });
const service = { widget: { type: "unifi_drive" } };
renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(screen.getAllByText("widget.api_error", { exact: false }).length).toBeGreaterThan(0);
});
it("renders no_data when storage data is missing", () => {
useWidgetAPI.mockReturnValue({ data: { data: null }, error: undefined });
const service = { widget: { type: "unifi_drive" } };
renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(screen.getByText("unifi_drive.no_data")).toBeInTheDocument();
});
it("renders storage statistics when data is loaded", () => {
useWidgetAPI.mockReturnValue({
data: {
data: {
totalQuota: 1000000000000,
usage: { system: 100000000000, myDrives: 200000000000, sharedDrives: 50000000000 },
status: "healthy",
},
},
error: undefined,
});
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "resources.total", 1000000000000);
expectBlockValue(container, "resources.used", 350000000000);
expectBlockValue(container, "resources.free", 650000000000);
expectBlockValue(container, "widget.status", "unifi_drive.healthy");
});
it("renders degraded status", () => {
useWidgetAPI.mockReturnValue({
data: {
data: {
totalQuota: 100,
usage: { system: 10, myDrives: 20, sharedDrives: 5 },
status: "degraded",
},
},
error: undefined,
});
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "widget.status", "unifi_drive.degraded");
expectBlockValue(container, "resources.free", 65);
});
});

View File

@@ -1,36 +0,0 @@
import getServiceWidget from "utils/config/service-helpers";
import createUnifiProxyHandler from "utils/proxy/handlers/unifi";
import { httpProxy } from "utils/proxy/http";
const drivePrefix = "/proxy/drive";
async function getWidget(req, logger) {
const { group, service, index } = req.query;
if (!group || !service) return null;
const widget = await getServiceWidget(group, service, index);
if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return null;
}
return widget;
}
async function resolveRequestContext({ cachedPrefix, widget }) {
if (cachedPrefix !== null) {
return { prefix: cachedPrefix };
}
const [, , , responseHeaders] = await httpProxy(widget.url);
return {
prefix: drivePrefix,
csrfToken: responseHeaders?.["x-csrf-token"],
};
}
export default createUnifiProxyHandler({
proxyName: "unifiDriveProxyHandler",
resolveWidget: getWidget,
resolveRequestContext,
});

View File

@@ -1,82 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, cache, logger } = vi.hoisted(() => {
const store = new Map();
return {
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
cache: {
get: vi.fn((k) => (store.has(k) ? store.get(k) : null)),
put: vi.fn((k, v) => store.set(k, v)),
del: vi.fn((k) => store.delete(k)),
_reset: () => store.clear(),
},
logger: { debug: vi.fn(), error: vi.fn() },
};
});
vi.mock("memory-cache", () => ({ default: cache, ...cache }));
vi.mock("utils/logger", () => ({ default: () => logger }));
vi.mock("utils/config/service-helpers", () => ({ default: getServiceWidget }));
vi.mock("utils/proxy/http", () => ({ httpProxy }));
vi.mock("widgets/widgets", () => ({
default: { unifi_drive: { api: "{url}{prefix}/api/{endpoint}" } },
}));
import unifiDriveProxyHandler from "./proxy";
const widgetConfig = { type: "unifi_drive", url: "http://unifi", username: "u", password: "p" };
describe("widgets/unifi_drive/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("returns 400 when widget config is missing", async () => {
getServiceWidget.mockResolvedValue(null);
const res = createMockRes();
await unifiDriveProxyHandler(
{ query: { group: "g", service: "s", endpoint: "v1/systems/storage?type=detail" } },
res,
);
expect(res.statusCode).toBe(400);
});
it("returns 403 when widget type has no API config", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig, type: "unknown" });
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(res.statusCode).toBe(403);
});
it("uses /proxy/drive prefix and returns data on success", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig });
httpProxy
.mockResolvedValueOnce([200, "text/html", Buffer.from(""), {}])
.mockResolvedValueOnce([200, "application/json", Buffer.from('{"data":{}}'), {}]);
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(httpProxy.mock.calls[0][0]).toBe("http://unifi");
expect(httpProxy.mock.calls[1][0].toString()).toContain("/proxy/drive/api/");
expect(cache.put).toHaveBeenCalledWith("unifiDriveProxyHandler__prefix.s", "/proxy/drive");
expect(res.statusCode).toBe(200);
});
it("skips prefix detection when cached", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig });
cache.put("unifiDriveProxyHandler__prefix.s", "/proxy/drive");
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from('{"data":{}}'), {}]);
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(httpProxy).toHaveBeenCalledTimes(1);
expect(httpProxy.mock.calls[0][0].toString()).toContain("/proxy/drive/api/");
expect(res.statusCode).toBe(200);
});
});

View File

@@ -1,14 +0,0 @@
import unifiDriveProxyHandler from "./proxy";
const widget = {
api: "{url}{prefix}/api/{endpoint}",
proxyHandler: unifiDriveProxyHandler,
mappings: {
storage: {
endpoint: "v1/systems/storage?type=detail",
},
},
};
export default widget;

View File

@@ -1,11 +0,0 @@
import { describe, it } from "vitest";
import { expectWidgetConfigShape } from "test-utils/widget-config";
import widget from "./widget";
describe("unifi_drive widget config", () => {
it("exports a valid widget config", () => {
expectWidgetConfigShape(widget);
});
});

View File

@@ -137,7 +137,6 @@ import trilium from "./trilium/widget";
import truenas from "./truenas/widget";
import tubearchivist from "./tubearchivist/widget";
import unifi from "./unifi/widget";
import unifi_drive from "./unifi_drive/widget";
import unmanic from "./unmanic/widget";
import unraid from "./unraid/widget";
import uptimekuma from "./uptimekuma/widget";
@@ -297,7 +296,6 @@ const widgets = {
truenas,
unifi,
unifi_console: unifi,
unifi_drive,
unmanic,
unraid,
uptimekuma,