mirror of
https://github.com/gethomepage/homepage.git
synced 2026-05-18 19:40:58 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233721cc90 | ||
|
|
d294a25145 | ||
|
|
0ff9af4c3c | ||
|
|
a6c753c8aa | ||
|
|
5d71c3aa65 | ||
|
|
1cc4608d72 | ||
|
|
d65e5447e4 | ||
|
|
d3256596d8 |
44
.github/release-drafter.yml
vendored
44
.github/release-drafter.yml
vendored
@@ -47,15 +47,34 @@ categories:
|
|||||||
- 'documentation'
|
- 'documentation'
|
||||||
|
|
||||||
autolabeler:
|
autolabeler:
|
||||||
|
- label: 'bug'
|
||||||
|
title:
|
||||||
|
- '/^fix(\(.+\))?:/i'
|
||||||
|
body:
|
||||||
|
- '/- \[[xX]\] Bug fix \(non-breaking change which fixes an issue\)/'
|
||||||
|
|
||||||
|
- label: 'enhancement'
|
||||||
|
title:
|
||||||
|
- '/^(feature|enhancement)(\(.+\))?:/i'
|
||||||
|
body:
|
||||||
|
- '/- \[[xX]\] New service widget/'
|
||||||
|
- '/- \[[xX]\] New feature or enhancement \(non-breaking change which adds functionality\)/'
|
||||||
|
|
||||||
- label: 'documentation'
|
- label: 'documentation'
|
||||||
files:
|
title:
|
||||||
- 'docs/**'
|
- '/^(documentation|docs)(\(.+\))?:/i'
|
||||||
- '*.md'
|
body:
|
||||||
- '.github/**/*.md'
|
- '/- \[[xX]\] Documentation only/'
|
||||||
|
|
||||||
|
- label: 'chore'
|
||||||
|
body:
|
||||||
|
- '/- \[[xX]\] Other \(please explain\)/'
|
||||||
|
|
||||||
- label: 'ci'
|
- label: 'ci'
|
||||||
files:
|
branch:
|
||||||
- '.github/workflows/**'
|
- '/^(ci|workflow|actions)\//'
|
||||||
|
title:
|
||||||
|
- '/^(ci|workflow|actions)(\(.+\))?:/i'
|
||||||
|
|
||||||
- label: 'dependencies'
|
- label: 'dependencies'
|
||||||
files:
|
files:
|
||||||
@@ -64,19 +83,6 @@ autolabeler:
|
|||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
|
|
||||||
- label: 'feature'
|
|
||||||
files:
|
|
||||||
- 'src/components/**'
|
|
||||||
- 'src/widgets/**'
|
|
||||||
- 'src/pages/**'
|
|
||||||
- 'src/utils/**'
|
|
||||||
|
|
||||||
- label: 'chore'
|
|
||||||
files:
|
|
||||||
- 'Dockerfile*'
|
|
||||||
- 'docker-entrypoint.sh'
|
|
||||||
- 'k3d/**'
|
|
||||||
|
|
||||||
- label: 'translation'
|
- label: 'translation'
|
||||||
files:
|
files:
|
||||||
- 'public/locales/**'
|
- 'public/locales/**'
|
||||||
|
|||||||
43
.github/workflows/issue-triage.yml
vendored
Normal file
43
.github/workflows/issue-triage.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Issue Triage
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-needs-discussion:
|
||||||
|
name: Issues Need Discussion
|
||||||
|
if: github.event.label.name == 'needs-discussion'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const issueNumber = context.payload.issue.number;
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body: 'This issue is being closed because it was opened before a maintainer asked for an issue to be created. Please start with a discussion and follow the issue template; only open an issue when a maintainer asks you to do so.',
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
state: 'closed',
|
||||||
|
state_reason: 'not_planned',
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.lock({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
lock_reason: 'off-topic',
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.13.0",
|
"version": "1.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
|||||||
@@ -344,15 +344,15 @@
|
|||||||
"address": "Adresse",
|
"address": "Adresse",
|
||||||
"expires": "Expire",
|
"expires": "Expire",
|
||||||
"never": "Jamais",
|
"never": "Jamais",
|
||||||
"user": "User",
|
"user": "Utilisateur",
|
||||||
"hostname": "Hostname",
|
"hostname": "Nom d'hôte",
|
||||||
"name": "Name",
|
"name": "Nom",
|
||||||
"client_version": "Client Version",
|
"client_version": "Version client",
|
||||||
"os": "OS",
|
"os": "OS",
|
||||||
"created": "Created",
|
"created": "Créé",
|
||||||
"authorized": "Authorized",
|
"authorized": "Autorisée",
|
||||||
"is_external": "Is External",
|
"is_external": "Externe",
|
||||||
"update_available": "Update Available",
|
"update_available": "Mise à jour disponible",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"last_seen": "Vu pour la dernière fois",
|
"last_seen": "Vu pour la dernière fois",
|
||||||
"now": "Maintenant",
|
"now": "Maintenant",
|
||||||
@@ -363,8 +363,8 @@
|
|||||||
"minutes": "{{number}}m",
|
"minutes": "{{number}}m",
|
||||||
"seconds": "{{number}}s",
|
"seconds": "{{number}}s",
|
||||||
"ago": "Il y a {{value}}",
|
"ago": "Il y a {{value}}",
|
||||||
"true": "Yes",
|
"true": "Oui",
|
||||||
"false": "No"
|
"false": "Non"
|
||||||
},
|
},
|
||||||
"technitium": {
|
"technitium": {
|
||||||
"totalQueries": "Requêtes",
|
"totalQueries": "Requêtes",
|
||||||
@@ -937,16 +937,16 @@
|
|||||||
"criticals": "Urgent"
|
"criticals": "Urgent"
|
||||||
},
|
},
|
||||||
"ntfy": {
|
"ntfy": {
|
||||||
"title": "Title",
|
"title": "Titre",
|
||||||
"priority": "Priority",
|
"priority": "Priorité",
|
||||||
"lastReceived": "Last Received",
|
"lastReceived": "Dernière réception",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"none": "None",
|
"none": "Aucun",
|
||||||
"min": "Min",
|
"min": "Min",
|
||||||
"low": "Low",
|
"low": "Bas",
|
||||||
"default": "Default",
|
"default": "Défaut",
|
||||||
"high": "High",
|
"high": "Haut",
|
||||||
"urgent": "Urgent"
|
"urgent": "Urgent"
|
||||||
},
|
},
|
||||||
"plantit": {
|
"plantit": {
|
||||||
|
|||||||
@@ -67,14 +67,14 @@
|
|||||||
"empty_data": "Статус подсистемы неизвестен"
|
"empty_data": "Статус подсистемы неизвестен"
|
||||||
},
|
},
|
||||||
"unifi_drive": {
|
"unifi_drive": {
|
||||||
"healthy": "Healthy",
|
"healthy": "Здоров",
|
||||||
"degraded": "Degraded",
|
"degraded": "Деградация",
|
||||||
"no_data": "No storage data available"
|
"no_data": "Нет доступных данных о хранилище"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
"tx": "TX",
|
"tx": "TX",
|
||||||
"mem": "Память",
|
"mem": "ОЗУ",
|
||||||
"cpu": "ЦП",
|
"cpu": "ЦП",
|
||||||
"running": "Запущено",
|
"running": "Запущено",
|
||||||
"offline": "Не в сети",
|
"offline": "Не в сети",
|
||||||
@@ -184,15 +184,15 @@
|
|||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "Играет",
|
"playing": "Играет",
|
||||||
"transcoding": "Транскодируется",
|
"transcoding": "Перекодируется",
|
||||||
"bitrate": "Битрейт",
|
"bitrate": "Битрейт",
|
||||||
"no_active": "Нет активных стримов",
|
"no_active": "Нет активных потоков",
|
||||||
"plex_connection_error": "Проверка соединения Plex"
|
"plex_connection_error": "Проверка соединения Plex"
|
||||||
},
|
},
|
||||||
"tracearr": {
|
"tracearr": {
|
||||||
"no_active": "Нет активных потоков",
|
"no_active": "Нет активных потоков",
|
||||||
"streams": "Потоки",
|
"streams": "Потоки",
|
||||||
"transcodes": "Transcodes",
|
"transcodes": "Перекодирования",
|
||||||
"directplay": "Прямое воспроизведение",
|
"directplay": "Прямое воспроизведение",
|
||||||
"bitrate": "Битрейт"
|
"bitrate": "Битрейт"
|
||||||
},
|
},
|
||||||
@@ -215,18 +215,18 @@
|
|||||||
"tv": "Сериалы"
|
"tv": "Сериалы"
|
||||||
},
|
},
|
||||||
"sabnzbd": {
|
"sabnzbd": {
|
||||||
"rate": "",
|
"rate": "Скорость",
|
||||||
"queue": "Очередь",
|
"queue": "Очередь",
|
||||||
"timeleft": "Осталось"
|
"timeleft": "Осталось"
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Активно",
|
"active": "Активно",
|
||||||
"upload": "Загрузка",
|
"upload": "Отдача",
|
||||||
"download": "Скачивание"
|
"download": "Скачивание"
|
||||||
},
|
},
|
||||||
"transmission": {
|
"transmission": {
|
||||||
"download": "Скачивание",
|
"download": "Скачивание",
|
||||||
"upload": "Загрузка",
|
"upload": "Отдача",
|
||||||
"leech": "Лич",
|
"leech": "Лич",
|
||||||
"seed": "Сид"
|
"seed": "Сид"
|
||||||
},
|
},
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
"available": "Доступно"
|
"available": "Доступно"
|
||||||
},
|
},
|
||||||
"seerr": {
|
"seerr": {
|
||||||
"pending": "Pending",
|
"pending": "Ожидают",
|
||||||
"approved": "Одобрено",
|
"approved": "Одобрено",
|
||||||
"available": "Доступно",
|
"available": "Доступно",
|
||||||
"completed": "Завершено",
|
"completed": "Завершено",
|
||||||
@@ -344,16 +344,16 @@
|
|||||||
"address": "Адрес",
|
"address": "Адрес",
|
||||||
"expires": "Истекает",
|
"expires": "Истекает",
|
||||||
"never": "Никогда",
|
"never": "Никогда",
|
||||||
"user": "User",
|
"user": "Пользователь",
|
||||||
"hostname": "Hostname",
|
"hostname": "Имя хоста",
|
||||||
"name": "Name",
|
"name": "Имя",
|
||||||
"client_version": "Client Version",
|
"client_version": "Версия клиента",
|
||||||
"os": "OS",
|
"os": "ОС",
|
||||||
"created": "Created",
|
"created": "Создано",
|
||||||
"authorized": "Authorized",
|
"authorized": "Авторизовано",
|
||||||
"is_external": "Is External",
|
"is_external": "Внешний",
|
||||||
"update_available": "Update Available",
|
"update_available": "Доступно обновление",
|
||||||
"tags": "Tags",
|
"tags": "Теги",
|
||||||
"last_seen": "Последнее посещение",
|
"last_seen": "Последнее посещение",
|
||||||
"now": "Только что",
|
"now": "Только что",
|
||||||
"years": "{{number}}г",
|
"years": "{{number}}г",
|
||||||
@@ -363,8 +363,8 @@
|
|||||||
"minutes": "{{number}}м",
|
"minutes": "{{number}}м",
|
||||||
"seconds": "{{number}}с",
|
"seconds": "{{number}}с",
|
||||||
"ago": "{{value}} назад",
|
"ago": "{{value}} назад",
|
||||||
"true": "Yes",
|
"true": "Да",
|
||||||
"false": "No"
|
"false": "Нет"
|
||||||
},
|
},
|
||||||
"technitium": {
|
"technitium": {
|
||||||
"totalQueries": "Запросы",
|
"totalQueries": "Запросы",
|
||||||
@@ -632,12 +632,12 @@
|
|||||||
},
|
},
|
||||||
"pangolin": {
|
"pangolin": {
|
||||||
"orgs": "Orgs",
|
"orgs": "Orgs",
|
||||||
"sites": "Sites",
|
"sites": "Сайты",
|
||||||
"resources": "Resources",
|
"resources": "Ресурсы",
|
||||||
"targets": "Targets",
|
"targets": "Цели",
|
||||||
"traffic": "Traffic",
|
"traffic": "Трафик",
|
||||||
"in": "In",
|
"in": "Входящий",
|
||||||
"out": "Out"
|
"out": "Исходящий"
|
||||||
},
|
},
|
||||||
"peanut": {
|
"peanut": {
|
||||||
"battery_charge": "Заряд батареи",
|
"battery_charge": "Заряд батареи",
|
||||||
@@ -736,8 +736,8 @@
|
|||||||
"volumeAvailable": "Доступно"
|
"volumeAvailable": "Доступно"
|
||||||
},
|
},
|
||||||
"dispatcharr": {
|
"dispatcharr": {
|
||||||
"channels": "Channels",
|
"channels": "Каналы",
|
||||||
"streams": "Streams"
|
"streams": "Потоки"
|
||||||
},
|
},
|
||||||
"mylar": {
|
"mylar": {
|
||||||
"series": "Серии",
|
"series": "Серии",
|
||||||
@@ -828,10 +828,10 @@
|
|||||||
"series": "Серии"
|
"series": "Серии"
|
||||||
},
|
},
|
||||||
"booklore": {
|
"booklore": {
|
||||||
"libraries": "Libraries",
|
"libraries": "Библиотеки",
|
||||||
"books": "Books",
|
"books": "Книги",
|
||||||
"reading": "Reading",
|
"reading": "Читаю",
|
||||||
"finished": "Finished"
|
"finished": "Завершено"
|
||||||
},
|
},
|
||||||
"jdownloader": {
|
"jdownloader": {
|
||||||
"downloadCount": "Очередь",
|
"downloadCount": "Очередь",
|
||||||
@@ -937,17 +937,17 @@
|
|||||||
"criticals": "Критические"
|
"criticals": "Критические"
|
||||||
},
|
},
|
||||||
"ntfy": {
|
"ntfy": {
|
||||||
"title": "Title",
|
"title": "Название",
|
||||||
"priority": "Priority",
|
"priority": "Приоритет",
|
||||||
"lastReceived": "Last Received",
|
"lastReceived": "Last Received",
|
||||||
"message": "Message",
|
"message": "Сообщение",
|
||||||
"tags": "Tags",
|
"tags": "Теги",
|
||||||
"none": "None",
|
"none": "Отсутствует",
|
||||||
"min": "Min",
|
"min": "Минимальный",
|
||||||
"low": "Low",
|
"low": "Низкий",
|
||||||
"default": "Default",
|
"default": "По-умолчанию",
|
||||||
"high": "High",
|
"high": "Высокий",
|
||||||
"urgent": "Urgent"
|
"urgent": "Срочный"
|
||||||
},
|
},
|
||||||
"plantit": {
|
"plantit": {
|
||||||
"events": "События",
|
"events": "События",
|
||||||
@@ -1087,7 +1087,7 @@
|
|||||||
},
|
},
|
||||||
"gitlab": {
|
"gitlab": {
|
||||||
"groups": "Группы",
|
"groups": "Группы",
|
||||||
"issues": "Issues",
|
"issues": "Задачи",
|
||||||
"merges": "Мердж-реквесты",
|
"merges": "Мердж-реквесты",
|
||||||
"projects": "Проекты"
|
"projects": "Проекты"
|
||||||
},
|
},
|
||||||
@@ -1150,26 +1150,26 @@
|
|||||||
"nextRenewingSubscription": "Следующая оплата"
|
"nextRenewingSubscription": "Следующая оплата"
|
||||||
},
|
},
|
||||||
"unraid": {
|
"unraid": {
|
||||||
"STARTED": "Started",
|
"STARTED": "Запущено",
|
||||||
"STOPPED": "Stopped",
|
"STOPPED": "Остановлено",
|
||||||
"NEW_ARRAY": "Новый массив",
|
"NEW_ARRAY": "Новый массив",
|
||||||
"RECON_DISK": "Reconstructing Disk",
|
"RECON_DISK": "Восстановление Диска",
|
||||||
"DISABLE_DISK": "Disk Disabled",
|
"DISABLE_DISK": "Диск отключен",
|
||||||
"SWAP_DSBL": "Swap Disable",
|
"SWAP_DSBL": "Swap отключён",
|
||||||
"INVALID_EXPANSION": "Invalid Expansion",
|
"INVALID_EXPANSION": "Неверное Расширение",
|
||||||
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
|
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
|
||||||
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
|
"TOO_MANY_MISSING_DISKS": "Слишком много отсутствующих дисков",
|
||||||
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
|
"NEW_DISK_TOO_SMALL": "Новый диск слишком мал",
|
||||||
"NO_DATA_DISKS": "No Data Disks",
|
"NO_DATA_DISKS": "Нет дисков данных",
|
||||||
"notifications": "Уведомления",
|
"notifications": "Уведомления",
|
||||||
"status": "Статус",
|
"status": "Статус",
|
||||||
"cpu": "ЦП",
|
"cpu": "ЦП",
|
||||||
"memoryUsed": "Использовано ОЗУ",
|
"memoryUsed": "Использовано ОЗУ",
|
||||||
"memoryAvailable": "Memory Available",
|
"memoryAvailable": "Доступная память",
|
||||||
"arrayUsed": "Array Used",
|
"arrayUsed": "Array Used",
|
||||||
"arrayFree": "Array Free",
|
"arrayFree": "Array Free",
|
||||||
"poolUsed": "{{pool}} Used",
|
"poolUsed": "{{pool}} Использовано",
|
||||||
"poolFree": "{{pool}} Free"
|
"poolFree": "{{pool}} Свободно"
|
||||||
},
|
},
|
||||||
"backrest": {
|
"backrest": {
|
||||||
"num_plans": "Plans",
|
"num_plans": "Plans",
|
||||||
@@ -1180,29 +1180,29 @@
|
|||||||
"bytes_added_30": "Bytes Added"
|
"bytes_added_30": "Bytes Added"
|
||||||
},
|
},
|
||||||
"yourspotify": {
|
"yourspotify": {
|
||||||
"songs": "Songs",
|
"songs": "Треков",
|
||||||
"time": "Время",
|
"time": "Время",
|
||||||
"artists": "Artists"
|
"artists": "Исполнителей"
|
||||||
},
|
},
|
||||||
"arcane": {
|
"arcane": {
|
||||||
"containers": "Containers",
|
"containers": "Контейнеров",
|
||||||
"images": "Images",
|
"images": "Образов",
|
||||||
"image_updates": "Image Updates",
|
"image_updates": "Обновлений",
|
||||||
"images_unused": "Unused",
|
"images_unused": "Не используется",
|
||||||
"environment_required": "Environment ID Required"
|
"environment_required": "Требуется ID окружения"
|
||||||
},
|
},
|
||||||
"dockhand": {
|
"dockhand": {
|
||||||
"running": "Running",
|
"running": "Запущено",
|
||||||
"stopped": "Stopped",
|
"stopped": "Остановлено",
|
||||||
"cpu": "CPU",
|
"cpu": "ЦП",
|
||||||
"memory": "Memory",
|
"memory": "ОЗУ",
|
||||||
"images": "Images",
|
"images": "Образов",
|
||||||
"volumes": "Volumes",
|
"volumes": "Томов",
|
||||||
"events_today": "Events Today",
|
"events_today": "Событий сегодня",
|
||||||
"pending_updates": "Pending Updates",
|
"pending_updates": "Обновлений",
|
||||||
"stacks": "Stacks",
|
"stacks": "Стеков",
|
||||||
"paused": "Paused",
|
"paused": "На паузе",
|
||||||
"total": "Total",
|
"total": "Всего",
|
||||||
"environment_not_found": "Среда не найдена"
|
"environment_not_found": "Среда не найдена"
|
||||||
},
|
},
|
||||||
"sparkyfitness": {
|
"sparkyfitness": {
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ export default function validateWidgetData(widget, endpoint, data) {
|
|||||||
let dataParsed = data;
|
let dataParsed = data;
|
||||||
let error;
|
let error;
|
||||||
let mapping;
|
let mapping;
|
||||||
|
const mappings = widgets[widget.type]?.mappings;
|
||||||
|
if (mappings) {
|
||||||
|
mapping = Object.values(mappings).find((m) => m.endpoint === endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping?.allowEmpty && Buffer.isBuffer(data) && data.length === 0) return true;
|
||||||
|
|
||||||
if (Buffer.isBuffer(data)) {
|
if (Buffer.isBuffer(data)) {
|
||||||
try {
|
try {
|
||||||
dataParsed = JSON.parse(data);
|
dataParsed = JSON.parse(data);
|
||||||
@@ -23,15 +30,11 @@ export default function validateWidgetData(widget, endpoint, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataParsed && Object.entries(dataParsed).length) {
|
if (dataParsed && Object.entries(dataParsed).length) {
|
||||||
const mappings = widgets[widget.type]?.mappings;
|
mapping?.validate?.forEach((key) => {
|
||||||
if (mappings) {
|
if (dataParsed[key] === undefined) {
|
||||||
mapping = Object.values(mappings).find((m) => m.endpoint === endpoint);
|
valid = false;
|
||||||
mapping?.validate?.forEach((key) => {
|
}
|
||||||
if (dataParsed[key] === undefined) {
|
});
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const { loggerError } = vi.hoisted(() => ({
|
const { loggerError } = vi.hoisted(() => ({
|
||||||
loggerError: vi.fn(),
|
loggerError: vi.fn(),
|
||||||
@@ -18,6 +18,10 @@ vi.mock("widgets/widgets", () => ({
|
|||||||
endpoint: "foo",
|
endpoint: "foo",
|
||||||
validate: ["a", "b"],
|
validate: ["a", "b"],
|
||||||
},
|
},
|
||||||
|
empty: {
|
||||||
|
endpoint: "empty",
|
||||||
|
allowEmpty: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -26,6 +30,10 @@ vi.mock("widgets/widgets", () => ({
|
|||||||
import validateWidgetData from "./validate-widget-data";
|
import validateWidgetData from "./validate-widget-data";
|
||||||
|
|
||||||
describe("utils/proxy/validate-widget-data", () => {
|
describe("utils/proxy/validate-widget-data", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("returns false when buffer JSON cannot be parsed", () => {
|
it("returns false when buffer JSON cannot be parsed", () => {
|
||||||
expect(validateWidgetData({ type: "test" }, "foo", Buffer.from("not json"))).toBe(false);
|
expect(validateWidgetData({ type: "test" }, "foo", Buffer.from("not json"))).toBe(false);
|
||||||
expect(loggerError).toHaveBeenCalled();
|
expect(loggerError).toHaveBeenCalled();
|
||||||
@@ -41,4 +49,9 @@ describe("utils/proxy/validate-widget-data", () => {
|
|||||||
expect(validateWidgetData({ type: "test" }, "foo", Buffer.from(JSON.stringify({ a: 1 })))).toBe(false);
|
expect(validateWidgetData({ type: "test" }, "foo", Buffer.from(JSON.stringify({ a: 1 })))).toBe(false);
|
||||||
expect(loggerError).toHaveBeenCalled();
|
expect(loggerError).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows empty buffer responses for mappings that explicitly allow them", () => {
|
||||||
|
expect(validateWidgetData({ type: "test" }, "empty", Buffer.from(""))).toBe(true);
|
||||||
|
expect(loggerError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
import { asJson } from "utils/proxy/api-helpers";
|
||||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
|
||||||
|
const noMessages = {
|
||||||
|
title: null,
|
||||||
|
message: null,
|
||||||
|
priority: 3,
|
||||||
|
time: null,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/{endpoint}",
|
api: "{url}/{endpoint}",
|
||||||
proxyHandler: credentialedProxyHandler,
|
proxyHandler: credentialedProxyHandler,
|
||||||
@@ -7,6 +16,14 @@ const widget = {
|
|||||||
mappings: {
|
mappings: {
|
||||||
messages: {
|
messages: {
|
||||||
endpoint: "{topic}/json?poll=1&since=latest",
|
endpoint: "{topic}/json?poll=1&since=latest",
|
||||||
|
allowEmpty: true,
|
||||||
|
map: (data) => {
|
||||||
|
if (Buffer.isBuffer(data) && data.length === 0) {
|
||||||
|
return noMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return asJson(data);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||||
|
|
||||||
@@ -8,4 +8,18 @@ describe("ntfy widget config", () => {
|
|||||||
it("exports a valid widget config", () => {
|
it("exports a valid widget config", () => {
|
||||||
expectWidgetConfigShape(widget);
|
expectWidgetConfigShape(widget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps an empty latest message response to the no messages state", () => {
|
||||||
|
expect(widget.mappings.messages.map(Buffer.from(""))).toEqual({
|
||||||
|
title: null,
|
||||||
|
message: null,
|
||||||
|
priority: 3,
|
||||||
|
time: null,
|
||||||
|
tags: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses latest message responses", () => {
|
||||||
|
expect(widget.mappings.messages.map(Buffer.from('{"message":"hello"}'))).toEqual({ message: "hello" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const widget = {
|
|||||||
},
|
},
|
||||||
"nodes/localhost/tasks": {
|
"nodes/localhost/tasks": {
|
||||||
endpoint: "nodes/localhost/tasks",
|
endpoint: "nodes/localhost/tasks",
|
||||||
|
params: ["errors", "limit", "since"],
|
||||||
},
|
},
|
||||||
"nodes/localhost/status": {
|
"nodes/localhost/status": {
|
||||||
endpoint: "nodes/localhost/status",
|
endpoint: "nodes/localhost/status",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||||
|
|
||||||
@@ -8,4 +8,8 @@ describe("proxmoxbackupserver widget config", () => {
|
|||||||
it("exports a valid widget config", () => {
|
it("exports a valid widget config", () => {
|
||||||
expectWidgetConfigShape(widget);
|
expectWidgetConfigShape(widget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requires failed task query params for the tasks endpoint", () => {
|
||||||
|
expect(widget.mappings["nodes/localhost/tasks"].params).toEqual(["errors", "limit", "since"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ export default async function qbittorrentProxyHandler(req, res) {
|
|||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
[status, data] = await login(widget);
|
[status, data] = await login(widget);
|
||||||
|
|
||||||
if (status !== 200) {
|
if (![200, 204].includes(status)) {
|
||||||
logger.error("HTTP %d logging in to qBittorrent. Data: %s", status, data);
|
logger.error("HTTP %d logging in to qBittorrent. Data: %s", status, data);
|
||||||
return res.status(status).end(data);
|
return res.status(status).end(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.toString() !== "Ok.") {
|
if (status === 200 && data.toString() !== "Ok.") {
|
||||||
logger.error("Error logging in to qBittorrent: Data: %s", data);
|
logger.error("Error logging in to qBittorrent: Data: %s", data);
|
||||||
return res.status(401).end(data);
|
return res.status(401).end(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,25 @@ describe("widgets/qbittorrent/proxy", () => {
|
|||||||
expect(res.body).toEqual(Buffer.from("data"));
|
expect(res.body).toEqual(Buffer.from("data"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts qBittorrent 5.2.0 no-content login responses", async () => {
|
||||||
|
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });
|
||||||
|
|
||||||
|
httpProxy
|
||||||
|
.mockResolvedValueOnce([403, "application/json", Buffer.from("nope")])
|
||||||
|
.mockResolvedValueOnce([204, null, Buffer.from("")])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
|
||||||
|
|
||||||
|
const req = { query: { group: "g", service: "svc", endpoint: "torrents/info", index: "0" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await qbittorrentProxyHandler(req, res);
|
||||||
|
|
||||||
|
expect(httpProxy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(httpProxy.mock.calls[1][0]).toBe("http://qb/api/v2/auth/login");
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toEqual(Buffer.from("data"));
|
||||||
|
});
|
||||||
|
|
||||||
it("returns 401 when login succeeds but response body is not Ok.", async () => {
|
it("returns 401 when login succeeds but response body is not Ok.", async () => {
|
||||||
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });
|
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user