diff --git a/docs/widgets/services/unifi-drive.md b/docs/widgets/services/unifi-drive.md
new file mode 100644
index 000000000..90843140e
--- /dev/null
+++ b/docs/widgets/services/unifi-drive.md
@@ -0,0 +1,24 @@
+---
+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.
diff --git a/mkdocs.yml b/mkdocs.yml
index b08277808..1933825c5 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -171,6 +171,7 @@ 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
diff --git a/public/locales/af/common.json b/public/locales/af/common.json
index 0fc63a043..c591fe4d8 100644
--- a/public/locales/af/common.json
+++ b/public/locales/af/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/ar/common.json b/public/locales/ar/common.json
index 4b61311a1..915a3a4f6 100644
--- a/public/locales/ar/common.json
+++ b/public/locales/ar/common.json
@@ -66,6 +66,11 @@
"wait": "Please wait",
"empty_data": "حالة النظام الفرعي غير معروفة"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "استقبال",
"tx": "ارسال",
diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json
index 516fbc93a..6a6fa0a07 100644
--- a/public/locales/bg/common.json
+++ b/public/locales/bg/common.json
@@ -66,6 +66,11 @@
"wait": "Моля изчакайте",
"empty_data": "Неизвестен статус на подсистема"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "ПЧ",
"tx": "ИЗ",
diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json
index 43d2b2830..293a81de7 100644
--- a/public/locales/ca/common.json
+++ b/public/locales/ca/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/cs/common.json b/public/locales/cs/common.json
index d1e67c0f0..b4f7b2b62 100644
--- a/public/locales/cs/common.json
+++ b/public/locales/cs/common.json
@@ -66,6 +66,11 @@
"wait": "Čekejte prosím",
"empty_data": "Stav podsystému neznámý"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
diff --git a/public/locales/da/common.json b/public/locales/da/common.json
index 0c920cbdb..62ee45db4 100644
--- a/public/locales/da/common.json
+++ b/public/locales/da/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/de/common.json b/public/locales/de/common.json
index c5fb9a67c..27d4a73c7 100644
--- a/public/locales/de/common.json
+++ b/public/locales/de/common.json
@@ -66,6 +66,11 @@
"wait": "Bitte warten",
"empty_data": "Subsystem-Status unbekannt"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
@@ -115,7 +120,7 @@
"movies": "Filme",
"series": "Serien",
"episodes": "Episoden",
- "songs": "Songs"
+ "songs": "Titel"
},
"esphome": {
"offline": "Offline",
@@ -185,10 +190,10 @@
"plex_connection_error": "Prüfe Plex-Verbindung"
},
"tracearr": {
- "no_active": "No Active Streams",
+ "no_active": "Keine aktiven Streams",
"streams": "Streams",
- "transcodes": "Transcodes",
- "directplay": "Direct Play",
+ "transcodes": "Transkodieren",
+ "directplay": "Direkte Wiedergabe",
"bitrate": "Bitrate"
},
"omada": {
@@ -290,10 +295,10 @@
"available": "Verfügbar"
},
"seerr": {
- "pending": "Pending",
- "approved": "Approved",
- "available": "Available",
- "completed": "Completed",
+ "pending": "Ausstehend",
+ "approved": "Bestätigt",
+ "available": "Verfügbar",
+ "completed": "Abgeschlossen",
"processing": "Processing",
"issues": "Open Issues"
},
@@ -811,7 +816,7 @@
"series": "Serien"
},
"booklore": {
- "libraries": "Libraries",
+ "libraries": "Bibliotheken",
"books": "Bücher",
"reading": "Reading",
"finished": "Finished"
@@ -1176,7 +1181,7 @@
"environment_not_found": "Umgebung nicht gefunden"
},
"sparkyfitness": {
- "eaten": "Eaten",
+ "eaten": "",
"burned": "Burned",
"remaining": "Remaining",
"steps": "Steps"
diff --git a/public/locales/el/common.json b/public/locales/el/common.json
index f6f64577f..51ed63e3a 100644
--- a/public/locales/el/common.json
+++ b/public/locales/el/common.json
@@ -66,6 +66,11 @@
"wait": "Please wait",
"empty_data": "Άγνωστη κατάσταση υποσυστήματος"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 66a6a34a7..9528341b8 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/eo/common.json b/public/locales/eo/common.json
index 2da22914a..598de6243 100644
--- a/public/locales/eo/common.json
+++ b/public/locales/eo/common.json
@@ -66,6 +66,11 @@
"wait": "Please wait",
"empty_data": "Subsistemostatuso nekonata"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
diff --git a/public/locales/es/common.json b/public/locales/es/common.json
index 6bd5fb263..c1d95aac4 100644
--- a/public/locales/es/common.json
+++ b/public/locales/es/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/eu/common.json b/public/locales/eu/common.json
index 2d2be6637..5bbf72aa0 100644
--- a/public/locales/eu/common.json
+++ b/public/locales/eu/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/fi/common.json b/public/locales/fi/common.json
index 42434aec7..057ecd95d 100644
--- a/public/locales/fi/common.json
+++ b/public/locales/fi/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json
index fa5279a01..94c48892b 100644
--- a/public/locales/fr/common.json
+++ b/public/locales/fr/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/he/common.json b/public/locales/he/common.json
index 2897e0ddd..4eadb695f 100644
--- a/public/locales/he/common.json
+++ b/public/locales/he/common.json
@@ -66,6 +66,11 @@
"wait": "נא להמתין",
"empty_data": "מצב תת-מערכת לא ידוע"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json
index 45f0ffc21..a91365abd 100644
--- a/public/locales/hi/common.json
+++ b/public/locales/hi/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/hr/common.json b/public/locales/hr/common.json
index d6b8147d9..4217b2ad8 100644
--- a/public/locales/hr/common.json
+++ b/public/locales/hr/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/hu/common.json b/public/locales/hu/common.json
index 9191e2d02..d3475519e 100644
--- a/public/locales/hu/common.json
+++ b/public/locales/hu/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/id/common.json b/public/locales/id/common.json
index 12a1b1061..753110daf 100644
--- a/public/locales/id/common.json
+++ b/public/locales/id/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/it/common.json b/public/locales/it/common.json
index 992e2aee7..d7c992812 100644
--- a/public/locales/it/common.json
+++ b/public/locales/it/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json
index 787598995..d78ee2dac 100644
--- a/public/locales/ja/common.json
+++ b/public/locales/ja/common.json
@@ -66,6 +66,11 @@
"wait": "お待ちください",
"empty_data": "サブシステムの状態は不明"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "受信済み",
"tx": "送信済み",
diff --git a/public/locales/ko/common.json b/public/locales/ko/common.json
index 060cb0996..3f8e3cd3e 100644
--- a/public/locales/ko/common.json
+++ b/public/locales/ko/common.json
@@ -66,6 +66,11 @@
"wait": "잠시만 기다려주세요",
"empty_data": "서브시스템 상태 알 수 없음"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "수신",
"tx": "송신",
@@ -108,14 +113,14 @@
"songs": "음악"
},
"jellyfin": {
- "playing": "Playing",
- "transcoding": "Transcoding",
- "bitrate": "Bitrate",
- "no_active": "No Active Streams",
- "movies": "Movies",
- "series": "Series",
- "episodes": "Episodes",
- "songs": "Songs"
+ "playing": "재생 중",
+ "transcoding": "트랜스코딩 중",
+ "bitrate": "비트레이트",
+ "no_active": "활성 스트림 없음",
+ "movies": "영상",
+ "series": "시리즈",
+ "episodes": "에피소드",
+ "songs": "음악"
},
"esphome": {
"offline": "오프라인",
@@ -185,11 +190,11 @@
"plex_connection_error": "Plex 연결 확인"
},
"tracearr": {
- "no_active": "No Active Streams",
- "streams": "Streams",
- "transcodes": "Transcodes",
- "directplay": "Direct Play",
- "bitrate": "Bitrate"
+ "no_active": "활성 스트림 없음",
+ "streams": "스트림",
+ "transcodes": "트랜스코드",
+ "directplay": "다이렉트 플레이",
+ "bitrate": "비트레이트"
},
"omada": {
"connectedAp": "연결된 AP",
@@ -290,12 +295,12 @@
"available": "이용 가능"
},
"seerr": {
- "pending": "Pending",
- "approved": "Approved",
- "available": "Available",
- "completed": "Completed",
- "processing": "Processing",
- "issues": "Open Issues"
+ "pending": "대기 중",
+ "approved": "승인됨",
+ "available": "사용 가능",
+ "completed": "완료됨",
+ "processing": "처리 중",
+ "issues": "열린 이슈"
},
"netalertx": {
"total": "전체",
@@ -546,7 +551,7 @@
"up": "업",
"pending": "대기 중",
"down": "다운",
- "ok": "Ok"
+ "ok": "확인"
},
"healthchecks": {
"new": "신규",
@@ -618,9 +623,9 @@
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
- "traffic": "Traffic",
- "in": "In",
- "out": "Out"
+ "traffic": "트래픽",
+ "in": "수신",
+ "out": "송신"
},
"peanut": {
"battery_charge": "배터리 충전",
@@ -719,8 +724,8 @@
"volumeAvailable": "사용 가능"
},
"dispatcharr": {
- "channels": "Channels",
- "streams": "Streams"
+ "channels": "채널",
+ "streams": "스트림"
},
"mylar": {
"series": "시리즈",
@@ -787,7 +792,7 @@
"gross_percent_today": "오늘",
"gross_percent_1y": "1년",
"gross_percent_max": "전체 기간",
- "net_worth": "Net Worth"
+ "net_worth": "순자산"
},
"audiobookshelf": {
"podcasts": "팟캐스트",
@@ -811,10 +816,10 @@
"series": "시리즈"
},
"booklore": {
- "libraries": "Libraries",
- "books": "Books",
- "reading": "Reading",
- "finished": "Finished"
+ "libraries": "라이브러리",
+ "books": "책",
+ "reading": "읽는 중",
+ "finished": "완료"
},
"jdownloader": {
"downloadCount": "대기열",
@@ -1150,30 +1155,30 @@
"bytes_added_30": "추가된 용량"
},
"yourspotify": {
- "songs": "Songs",
- "time": "Time",
- "artists": "Artists"
+ "songs": "음악",
+ "time": "시간",
+ "artists": "아티스트"
},
"arcane": {
- "containers": "Containers",
- "images": "Images",
- "image_updates": "Image Updates",
- "images_unused": "Unused",
- "environment_required": "Environment ID Required"
+ "containers": "컨테이너",
+ "images": "이미지",
+ "image_updates": "이미지 업데이트",
+ "images_unused": "미사용",
+ "environment_required": "환경 ID 필요"
},
"dockhand": {
- "running": "Running",
- "stopped": "Stopped",
+ "running": "실행 중",
+ "stopped": "정지됨",
"cpu": "CPU",
- "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"
+ "memory": "메모리",
+ "images": "이미지",
+ "volumes": "볼륨",
+ "events_today": "오늘의 이벤트",
+ "pending_updates": "대기 중인 업데이트",
+ "stacks": "스택",
+ "paused": "일시정지됨",
+ "total": "전체",
+ "environment_not_found": "환경 없음"
},
"sparkyfitness": {
"eaten": "Eaten",
diff --git a/public/locales/lv/common.json b/public/locales/lv/common.json
index 411b95249..fb269367a 100644
--- a/public/locales/lv/common.json
+++ b/public/locales/lv/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/ms/common.json b/public/locales/ms/common.json
index 48af4fc1b..cb4772a86 100644
--- a/public/locales/ms/common.json
+++ b/public/locales/ms/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json
index b5bcee4ae..8bbbd0b57 100644
--- a/public/locales/nl/common.json
+++ b/public/locales/nl/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/no/common.json b/public/locales/no/common.json
index a07e6ff23..4214c6860 100644
--- a/public/locales/no/common.json
+++ b/public/locales/no/common.json
@@ -66,6 +66,11 @@
"wait": "Please wait",
"empty_data": "Ukjent undersystemstatus"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
diff --git a/public/locales/pl/common.json b/public/locales/pl/common.json
index e11b48569..ec0f54bed 100644
--- a/public/locales/pl/common.json
+++ b/public/locales/pl/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json
index 0275558fd..d9d056c87 100644
--- a/public/locales/pt/common.json
+++ b/public/locales/pt/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/pt_BR/common.json b/public/locales/pt_BR/common.json
index 67669eb18..c208f3055 100644
--- a/public/locales/pt_BR/common.json
+++ b/public/locales/pt_BR/common.json
@@ -14,7 +14,7 @@
"date": "{{value, date}}",
"relativeDate": "{{value, relativeDate}}",
"duration": "{{value, duration}}",
- "months": "M",
+ "months": "mo",
"days": "d",
"hours": "h",
"minutes": "m",
@@ -66,9 +66,14 @@
"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",
@@ -101,21 +106,21 @@
"playing": "A reproduzir",
"transcoding": "Transcodificação",
"bitrate": "Taxa de bits",
- "no_active": "Sem Streams Ativos",
+ "no_active": "Sem Transmissões Ativas",
"movies": "Filmes",
"series": "Séries",
"episodes": "Episódios",
"songs": "Canções"
},
"jellyfin": {
- "playing": "Playing",
+ "playing": "Jogando",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
- "movies": "Movies",
- "series": "Series",
- "episodes": "Episodes",
- "songs": "Songs"
+ "movies": "Filmes",
+ "series": "Séries",
+ "episodes": "Episódios",
+ "songs": "Músicas"
},
"esphome": {
"offline": "Offline",
@@ -290,12 +295,12 @@
"available": "Disponível"
},
"seerr": {
- "pending": "Pending",
- "approved": "Approved",
- "available": "Available",
- "completed": "Completed",
- "processing": "Processing",
- "issues": "Open Issues"
+ "pending": "Pendente",
+ "approved": "Aprovado",
+ "available": "Disponível",
+ "completed": "Concluído",
+ "processing": "Processando",
+ "issues": "Erros pendentes"
},
"netalertx": {
"total": "Total",
@@ -616,7 +621,7 @@
"pangolin": {
"orgs": "Orgs",
"sites": "Sites",
- "resources": "Resources",
+ "resources": "Recursos",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
@@ -719,8 +724,8 @@
"volumeAvailable": "Disponível"
},
"dispatcharr": {
- "channels": "Channels",
- "streams": "Streams"
+ "channels": "Canais",
+ "streams": "Transmissões"
},
"mylar": {
"series": "Séries",
@@ -811,10 +816,10 @@
"series": "Séries"
},
"booklore": {
- "libraries": "Libraries",
- "books": "Books",
- "reading": "Reading",
- "finished": "Finished"
+ "libraries": "Bibliotecas",
+ "books": "Livros",
+ "reading": "Lendo",
+ "finished": "Finalizado"
},
"jdownloader": {
"downloadCount": "Fila de espera",
@@ -1155,23 +1160,23 @@
"artists": "Artistas"
},
"arcane": {
- "containers": "Containers",
- "images": "Images",
- "image_updates": "Image Updates",
- "images_unused": "Unused",
+ "containers": "Recipientes",
+ "images": "Imagens",
+ "image_updates": "Atualizações de Imagem",
+ "images_unused": "Não utilizado",
"environment_required": "Environment ID Required"
},
"dockhand": {
- "running": "Running",
+ "running": "Executando",
"stopped": "Stopped",
"cpu": "CPU",
- "memory": "Memory",
- "images": "Images",
- "volumes": "Volumes",
- "events_today": "Events Today",
- "pending_updates": "Pending Updates",
- "stacks": "Stacks",
- "paused": "Paused",
+ "memory": "Memória",
+ "images": "Imagens",
+ "volumes": "Quantidades",
+ "events_today": "Eventos hoje",
+ "pending_updates": "Atualizações pendentes",
+ "stacks": "Pilhas",
+ "paused": "Pausado",
"total": "Total",
"environment_not_found": "Environment Not Found"
},
diff --git a/public/locales/ro/common.json b/public/locales/ro/common.json
index 813dcc45e..1f415d796 100644
--- a/public/locales/ro/common.json
+++ b/public/locales/ro/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json
index 0fa464fc7..c58d1f220 100644
--- a/public/locales/ru/common.json
+++ b/public/locales/ru/common.json
@@ -66,6 +66,11 @@
"wait": "Пожалуйста, подождите",
"empty_data": "Статус подсистемы неизвестен"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
diff --git a/public/locales/sk/common.json b/public/locales/sk/common.json
index 917d70923..57359f65d 100644
--- a/public/locales/sk/common.json
+++ b/public/locales/sk/common.json
@@ -66,6 +66,11 @@
"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é",
diff --git a/public/locales/sl/common.json b/public/locales/sl/common.json
index bcc63b1b7..ae1ad2d4a 100644
--- a/public/locales/sl/common.json
+++ b/public/locales/sl/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/sr/common.json b/public/locales/sr/common.json
index 110217cd3..6c9efb6f7 100644
--- a/public/locales/sr/common.json
+++ b/public/locales/sr/common.json
@@ -66,6 +66,11 @@
"wait": "Молим сачекајте",
"empty_data": "Статус подсистема непознат"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json
index ef92e38b0..47ada9e49 100644
--- a/public/locales/sv/common.json
+++ b/public/locales/sv/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/te/common.json b/public/locales/te/common.json
index f027f0f5a..c5194fbc2 100644
--- a/public/locales/te/common.json
+++ b/public/locales/te/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/th/common.json b/public/locales/th/common.json
index b38d9db4a..419c9c247 100644
--- a/public/locales/th/common.json
+++ b/public/locales/th/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json
index a48822f63..fb90b25e5 100644
--- a/public/locales/tr/common.json
+++ b/public/locales/tr/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/uk/common.json b/public/locales/uk/common.json
index 9775de051..f45057d7a 100644
--- a/public/locales/uk/common.json
+++ b/public/locales/uk/common.json
@@ -66,6 +66,11 @@
"wait": "Будь ласка, зачекайте",
"empty_data": "Статус підсистеми невідомий"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "RX",
"tx": "TX",
diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json
index 6efa0a65f..d700bfb77 100644
--- a/public/locales/vi/common.json
+++ b/public/locales/vi/common.json
@@ -66,6 +66,11 @@
"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",
diff --git a/public/locales/yue/common.json b/public/locales/yue/common.json
index 4e9ed87a5..d65cb96f0 100644
--- a/public/locales/yue/common.json
+++ b/public/locales/yue/common.json
@@ -66,6 +66,11 @@
"wait": "Please wait",
"empty_data": "子系統狀態未知"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "接收",
"tx": "發送",
diff --git a/public/locales/zh-Hans/common.json b/public/locales/zh-Hans/common.json
index dab59e434..295196b22 100644
--- a/public/locales/zh-Hans/common.json
+++ b/public/locales/zh-Hans/common.json
@@ -66,6 +66,11 @@
"wait": "请稍候",
"empty_data": "子系统状态未知"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "接收",
"tx": "发送",
diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json
index 32c812862..ef0485202 100644
--- a/public/locales/zh-Hant/common.json
+++ b/public/locales/zh-Hant/common.json
@@ -66,6 +66,11 @@
"wait": "Please wait",
"empty_data": "子系統狀態未知"
},
+ "unifi_drive": {
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "no_data": "No storage data available"
+ },
"docker": {
"rx": "接收",
"tx": "傳送",
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 472ddd684..c5f144e39 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -147,6 +147,7 @@ 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")),
diff --git a/src/widgets/unifi_drive/component.jsx b/src/widgets/unifi_drive/component.jsx
new file mode 100644
index 000000000..a616dbd20
--- /dev/null
+++ b/src/widgets/unifi_drive/component.jsx
@@ -0,0 +1,58 @@
+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 ;
+ }
+
+ if (!storageData) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ const { data: storage } = storageData;
+
+ if (!storage) {
+ return (
+
+
+
+ );
+ }
+
+ 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 (
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/unifi_drive/component.test.jsx b/src/widgets/unifi_drive/component.test.jsx
new file mode 100644
index 000000000..46f23ce1e
--- /dev/null
+++ b/src/widgets/unifi_drive/component.test.jsx
@@ -0,0 +1,92 @@
+// @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(, { 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(, { 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(, { 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(, { 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(, { settings: { hideErrors: false } });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expectBlockValue(container, "widget.status", "unifi_drive.degraded");
+ expectBlockValue(container, "resources.free", 65);
+ });
+});
diff --git a/src/widgets/unifi_drive/proxy.js b/src/widgets/unifi_drive/proxy.js
new file mode 100644
index 000000000..02dff2ef3
--- /dev/null
+++ b/src/widgets/unifi_drive/proxy.js
@@ -0,0 +1,36 @@
+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,
+});
diff --git a/src/widgets/unifi_drive/proxy.test.js b/src/widgets/unifi_drive/proxy.test.js
new file mode 100644
index 000000000..07eef7480
--- /dev/null
+++ b/src/widgets/unifi_drive/proxy.test.js
@@ -0,0 +1,82 @@
+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);
+ });
+});
diff --git a/src/widgets/unifi_drive/widget.js b/src/widgets/unifi_drive/widget.js
new file mode 100644
index 000000000..4e78c5a5f
--- /dev/null
+++ b/src/widgets/unifi_drive/widget.js
@@ -0,0 +1,14 @@
+import unifiDriveProxyHandler from "./proxy";
+
+const widget = {
+ api: "{url}{prefix}/api/{endpoint}",
+ proxyHandler: unifiDriveProxyHandler,
+
+ mappings: {
+ storage: {
+ endpoint: "v1/systems/storage?type=detail",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/unifi_drive/widget.test.js b/src/widgets/unifi_drive/widget.test.js
new file mode 100644
index 000000000..87f60f69e
--- /dev/null
+++ b/src/widgets/unifi_drive/widget.test.js
@@ -0,0 +1,11 @@
+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);
+ });
+});
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 533410bdc..be7f685e4 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -137,6 +137,7 @@ 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";
@@ -296,6 +297,7 @@ const widgets = {
truenas,
unifi,
unifi_console: unifi,
+ unifi_drive,
unmanic,
unraid,
uptimekuma,