mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-06 11:36:50 +03:00
Compare commits
179 Commits
0.6.2
...
56829c74c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56829c74c8 | ||
|
|
9d78cd2ce4 | ||
|
|
d9ce3b361e | ||
|
|
c67aadf267 | ||
|
|
ac4d7570f3 | ||
|
|
86897fd0af | ||
|
|
230ffbce46 | ||
|
|
dd5ddd1a14 | ||
|
|
cc947f9734 | ||
|
|
f8510cd828 | ||
|
|
23cbe7be4a | ||
|
|
f168fb7e31 | ||
|
|
fe84b3154f | ||
|
|
d09fdc0b95 | ||
|
|
835cd85970 | ||
|
|
8a3b41ec9c | ||
|
|
10d7617739 | ||
|
|
68010ed5f7 | ||
|
|
557e3666eb | ||
|
|
01bff8ccfb | ||
|
|
675a6af89c | ||
|
|
f1a6ff3469 | ||
|
|
d4b3377d68 | ||
|
|
d2ef640d76 | ||
|
|
47457f2c27 | ||
|
|
8a29e176f2 | ||
|
|
9653310208 | ||
|
|
3540610c78 | ||
|
|
fb54d62a7f | ||
|
|
288b8d4cc2 | ||
|
|
e014396ae2 | ||
|
|
694e4ca35a | ||
|
|
788c539e16 | ||
|
|
743cba8936 | ||
|
|
d1d703764c | ||
|
|
2efd415305 | ||
|
|
407b19b3ed | ||
|
|
c3fac995d5 | ||
|
|
21ecfbbeca | ||
|
|
2918487845 | ||
|
|
ac258c53c0 | ||
|
|
9a389c47bf | ||
|
|
7cd70468c5 | ||
|
|
13d27dab21 | ||
|
|
9f8f032dce | ||
|
|
8301f4c271 | ||
|
|
c4078c8242 | ||
|
|
e0d149f03a | ||
|
|
0f77867ca2 | ||
|
|
fb5ae9c1e8 | ||
|
|
9e9bd5a2bd | ||
|
|
005574a01f | ||
|
|
a4bddeb430 | ||
|
|
d335d59f1b | ||
|
|
272ce012d7 | ||
|
|
64aa28f4e4 | ||
|
|
e89f89ea96 | ||
|
|
8fb8aad53b | ||
|
|
c1311fdd4b | ||
|
|
2cbaa888b2 | ||
|
|
25bb2355aa | ||
|
|
a2eac6f103 | ||
|
|
b5eec292e0 | ||
|
|
5573fce1b1 | ||
|
|
a3ac01478f | ||
|
|
2fb38286bd | ||
|
|
ac82cc1770 | ||
|
|
e8a3725948 | ||
|
|
686841c2a1 | ||
|
|
3379764ada | ||
|
|
1acdbe67a2 | ||
|
|
3bccf8d617 | ||
|
|
8384e18a22 | ||
|
|
b78682919a | ||
|
|
e8a5d3d5cc | ||
|
|
ed7b7e9c6d | ||
|
|
f4be831b5e | ||
|
|
4186292aa7 | ||
|
|
ef70f4e53d | ||
|
|
f0290fcc9e | ||
|
|
49dd1d608f | ||
|
|
9c01c8e2dd | ||
|
|
d0b06dd829 | ||
|
|
024c258d92 | ||
|
|
33b44fd9b3 | ||
|
|
8ff9562dcf | ||
|
|
9d5cdc3e90 | ||
|
|
72ad10d737 | ||
|
|
e7f3d15bce | ||
|
|
c0e3e256e3 | ||
|
|
08615b6f04 | ||
|
|
9d4c37b9a2 | ||
|
|
13f15dcf11 | ||
|
|
213b4603b7 | ||
|
|
f6e347af78 | ||
|
|
7ab0384e0b | ||
|
|
4d4164ae6f | ||
|
|
f155d6a118 | ||
|
|
96039f92a9 | ||
|
|
fd64eb5bcb | ||
|
|
d7235e8c06 | ||
|
|
30b30dcca6 | ||
|
|
97ab638b31 | ||
|
|
7dd3f33284 | ||
|
|
02a49ed067 | ||
|
|
af36cf3026 | ||
|
|
cfb821974f | ||
|
|
40dac07b29 | ||
|
|
d8b7e12c4d | ||
|
|
c0b35c865d | ||
|
|
c35a174708 | ||
|
|
b2a6971700 | ||
|
|
46ec79e003 | ||
|
|
d51ac63c94 | ||
|
|
53b71ec4b0 | ||
|
|
5087be83d3 | ||
|
|
6772b83861 | ||
|
|
b8ccb4abfa | ||
|
|
739e0d2ba7 | ||
|
|
ffa0073441 | ||
|
|
7cd32910d9 | ||
|
|
67ec5f3090 | ||
|
|
33dfb8c3f0 | ||
|
|
de3e67f999 | ||
|
|
a9fdf286e0 | ||
|
|
dbf7e39599 | ||
|
|
fa152c3abf | ||
|
|
661ba64879 | ||
|
|
953b669520 | ||
|
|
3f6f03c8d1 | ||
|
|
d39ee3a666 | ||
|
|
45bd2d0499 | ||
|
|
85b1dc75f5 | ||
|
|
f7517e6794 | ||
|
|
2e257e4adf | ||
|
|
74edbcf07f | ||
|
|
aea6fd9453 | ||
|
|
0fba31c10a | ||
|
|
a7150f7143 | ||
|
|
44894f3257 | ||
|
|
f20e205b72 | ||
|
|
7a2868b630 | ||
|
|
55df0f283d | ||
|
|
e3e0b2d4e4 | ||
|
|
4334643e8e | ||
|
|
5486dfb0a4 | ||
|
|
fd0b981186 | ||
|
|
d041334d88 | ||
|
|
791cc1c945 | ||
|
|
63d56e736d | ||
|
|
a33b53743f | ||
|
|
3d12327868 | ||
|
|
1bdd49e198 | ||
|
|
b90f520c68 | ||
|
|
7bfb673b49 | ||
|
|
ee93c26098 | ||
|
|
f95d801d44 | ||
|
|
ca5a3a79fe | ||
|
|
f128bc4ec7 | ||
|
|
458fd9251a | ||
|
|
35d9441837 | ||
|
|
e3557f374e | ||
|
|
1e6b555bfa | ||
|
|
036808917d | ||
|
|
687334bf8d | ||
|
|
095b3c6fa9 | ||
|
|
ba69e3eacc | ||
|
|
9be0eb3e57 | ||
|
|
d3847db313 | ||
|
|
ba91c180e8 | ||
|
|
8a80df9dc0 | ||
|
|
d2f0de39d9 | ||
|
|
e662f25f53 | ||
|
|
3042a86412 | ||
|
|
9f1505db48 | ||
|
|
34404f6e40 | ||
|
|
9e0135983f | ||
|
|
d176f24a7f | ||
|
|
acd1ca1bcb |
49
.github/workflows/shellcheck.yml
vendored
Normal file
49
.github/workflows/shellcheck.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: Differential ShellCheck
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'install.sh'
|
||||||
|
- 'podkop/files/usr/bin/**'
|
||||||
|
- 'podkop/files/usr/lib/**'
|
||||||
|
- '.github/workflows/shellcheck.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'install.sh'
|
||||||
|
- 'podkop/files/usr/bin/**'
|
||||||
|
- 'podkop/files/usr/lib/**'
|
||||||
|
- '.github/workflows/shellcheck.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
shellcheck:
|
||||||
|
name: Differential ShellCheck
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Differential ShellCheck
|
||||||
|
uses: redhat-plumbers-in-action/differential-shellcheck@v5.5.5
|
||||||
|
with:
|
||||||
|
severity: error
|
||||||
|
include-path: |
|
||||||
|
podkop/files/usr/bin/podkop
|
||||||
|
podkop/files/usr/lib/**.sh
|
||||||
|
install.sh
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
fe-app-podkop/node_modules
|
fe-app-podkop/node_modules
|
||||||
fe-app-podkop/.env
|
fe-app-podkop/.env
|
||||||
|
.DS_Store
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -1,16 +1,17 @@
|
|||||||
# Вещи, которые вам нужно знать перед установкой
|
# Вещи, которые вам нужно знать перед установкой
|
||||||
|
|
||||||
- Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
|
- Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
|
||||||
- При возникновении проблем, нужен технически грамотный фидбэк в чат.
|
- При возникновении проблем, нужен технически грамотный фидбэк в чат. Ознакомьтесь с закрепом в топике.
|
||||||
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
|
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
|
||||||
- Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
|
- Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
|
||||||
- Необходимо минимум 25МБ свободного места на роутере. Роутеры с флешками на 16МБ сразу мимо.
|
- Необходимо минимум 25МБ свободного места на роутере. Роутеры с флешками на 16МБ сразу мимо.
|
||||||
- При старте программы редактируется конфиг Dnsmasq.
|
- При старте программы редактируется конфиг Dnsmasq.
|
||||||
- Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен.
|
- Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен.
|
||||||
- Информация здесь может быть устаревшей. Все изменения фиксируются в [телеграм-чате](https://t.me/itdogchat/81758/420321).
|
- Информация здесь может быть устаревшей. Все изменения фиксируются в [телеграм-чате](https://t.me/itdogchat/81758/420321).
|
||||||
- [Если у вас не что-то не работает.](https://podkop.net/docs/diagnostics/)
|
- [Если у вас что-то не работает.](https://podkop.net/docs/diagnostics/)
|
||||||
- Если у вас установлен Getdomains, [его следует удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82-%D0%B4%D0%BB%D1%8F-%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F).
|
- Если у вас установлен Getdomains, [его следует удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82-%D0%B4%D0%BB%D1%8F-%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F).
|
||||||
- Требуется версия OpenWrt 24.10.
|
- Требуется версия OpenWrt 24.10.
|
||||||
|
- Dashboard доступен, если вы заходите по http (из-за особенностей clash api). И не будет работать, если вы заходите по https и/или домену.
|
||||||
|
|
||||||
# Документация
|
# Документация
|
||||||
https://podkop.net/
|
https://podkop.net/
|
||||||
@@ -23,33 +24,38 @@ https://podkop.net/
|
|||||||
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
|
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Изменения 0.7.0
|
||||||
|
Начиная с версии 0.7.0 изменена структура конфига `/etc/config/podkop`. Старые значения несовместимы с новыми. Нужно заново настроить Podkop.
|
||||||
|
|
||||||
|
Скрипт установки обнаружит старую версию и предупредит вас об этом. Если вы согласитесь, то он сделает автоматически написанное ниже.
|
||||||
|
|
||||||
|
При обновлении вручную нужно:
|
||||||
|
|
||||||
|
0. Не ныть в issue и чатик.
|
||||||
|
1. Забэкапить старый конфиг:
|
||||||
|
```
|
||||||
|
mv /etc/config/podkop /etc/config/podkop-070
|
||||||
|
```
|
||||||
|
2. Стянуть новый дефолтный конфиг:
|
||||||
|
```
|
||||||
|
wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop
|
||||||
|
```
|
||||||
|
3. Настроить заново ваш Podkop через Luci или UCI.
|
||||||
|
|
||||||
# ToDo
|
# ToDo
|
||||||
Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме.
|
|
||||||
|
|
||||||
Основные задачи в issues.
|
> [!IMPORTANT]
|
||||||
|
> PR принимаются только по issues, у которых стоит label "enhancement". Либо по согласованию с авторами в ТГ-чате. Остальные PR на данный момент не рассматриваются.
|
||||||
## Рефактор
|
|
||||||
- [x] Очевидные повторения в `/usr/bin/podkop` загнать в переменые
|
|
||||||
- [x] Возможно поменять структуру
|
|
||||||
|
|
||||||
## Списки
|
|
||||||
- [x] CloudFront
|
|
||||||
- [x] DO
|
|
||||||
- [x] HODCA
|
|
||||||
|
|
||||||
## Будущее
|
## Будущее
|
||||||
- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров.
|
- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров.
|
||||||
- [x] Опция, когда все запросы (с роутера в первую очередь), а не только br-lan идут в прокси. С этим связана #95. Требуется много переделать для nftables.
|
- [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне.
|
||||||
- [ ] Весь трафик в Proxy\VPN. Вопрос, что делать с экстрасекциями в этом случае. FakeIP здесь скорее не нужен, а значит только main секция остаётся. Всё что касается fakeip проверок, придётся выключать в этом режиме.
|
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусственно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111)
|
||||||
- [x] Поддержка Source format. Нужна расшифровка в json и если присуствуют подсети, заносить их в custom subnet nftset.
|
|
||||||
- [x] Переделывание функции формирования кастомных списков в JSON. Обрабатывать сразу скопом, а не по одному.
|
|
||||||
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусcтвенно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111)
|
|
||||||
- [x] Формирование конфига sing-box в /tmp
|
|
||||||
- [ ] Галочка, которая режет доступ к doh серверам.
|
- [ ] Галочка, которая режет доступ к doh серверам.
|
||||||
- [ ] IPv6. Только после наполнения Wiki.
|
- [ ] IPv6. Только после наполнения Wiki.
|
||||||
|
|
||||||
## Тесты
|
## Тесты
|
||||||
- [ ] Unit тесты (BATS)
|
- [ ] Unit тесты (BATS)
|
||||||
- [ ] Интеграционые тесты бекенда (OpenWrt rootfs + BATS)
|
- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS)
|
||||||
|
|
||||||
[](https://deepwiki.com/itdoginfo/podkop)
|
[](https://deepwiki.com/itdoginfo/podkop)
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
|
## Socks
|
||||||
|
```
|
||||||
|
socks4://127.0.0.1:1080
|
||||||
|
socks4a://127.0.0.1:1080
|
||||||
|
socks5://127.0.0.1:1080
|
||||||
|
socks5://username:password@127.0.0.1:1080
|
||||||
|
```
|
||||||
|
|
||||||
## Shadowsocks
|
## Shadowsocks
|
||||||
```
|
```
|
||||||
ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client
|
ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client
|
||||||
|
|||||||
16
fe-app-podkop/.env.example
Normal file
16
fe-app-podkop/.env.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
SFTP_HOST=192.168.160.129
|
||||||
|
SFTP_PORT=22
|
||||||
|
SFTP_USER=root
|
||||||
|
SFTP_PASS=
|
||||||
|
|
||||||
|
# you can use key if needed
|
||||||
|
# SFTP_PRIVATE_KEY=~/.ssh/id_rsa
|
||||||
|
|
||||||
|
LOCAL_DIR_FE=../luci-app-podkop/htdocs/luci-static/resources/view/podkop
|
||||||
|
REMOTE_DIR_FE=/www/luci-static/resources/view/podkop
|
||||||
|
|
||||||
|
LOCAL_DIR_BIN=../podkop/files/usr/bin/
|
||||||
|
REMOTE_DIR_BIN=/usr/bin/
|
||||||
|
|
||||||
|
LOCAL_DIR_LIB=../podkop/files/usr/lib/
|
||||||
|
REMOTE_DIR_LIB=/usr/lib/podkop/
|
||||||
38
fe-app-podkop/distribute-locales.js
Normal file
38
fe-app-podkop/distribute-locales.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const sourceDir = path.resolve(__dirname, 'locales');
|
||||||
|
const targetRoot = path.resolve(__dirname, '../luci-app-podkop/po');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const files = await fs.readdir(sourceDir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(sourceDir, file);
|
||||||
|
|
||||||
|
if (file === 'podkop.pot') {
|
||||||
|
const potTarget = path.join(targetRoot, 'templates', 'podkop.pot');
|
||||||
|
await fs.mkdir(path.dirname(potTarget), { recursive: true });
|
||||||
|
await fs.copyFile(filePath, potTarget);
|
||||||
|
console.log(`✅ Copied POT: ${filePath} → ${potTarget}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = file.match(/^podkop\.([a-zA-Z_]+)\.po$/);
|
||||||
|
if (match) {
|
||||||
|
const lang = match[1];
|
||||||
|
const poTarget = path.join(targetRoot, lang, 'podkop.po');
|
||||||
|
await fs.mkdir(path.dirname(poTarget), { recursive: true });
|
||||||
|
await fs.copyFile(filePath, poTarget);
|
||||||
|
console.log(`✅ Copied ${lang.toUpperCase()}: ${filePath} → ${poTarget}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('❌ Ошибка при распространении переводов:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
75
fe-app-podkop/extract-calls.js
Normal file
75
fe-app-podkop/extract-calls.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import glob from 'fast-glob';
|
||||||
|
import { parse } from '@babel/parser';
|
||||||
|
import traverse from '@babel/traverse';
|
||||||
|
import * as t from '@babel/types';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
function stripIllegalReturn(code) {
|
||||||
|
return code.replace(/^\s*return\s+[^;]+;\s*$/gm, (match, offset, input) => {
|
||||||
|
const after = input.slice(offset + match.length).trim();
|
||||||
|
return after === '' ? '' : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await glob([
|
||||||
|
'src/**/*.ts',
|
||||||
|
'../luci-app-podkop/htdocs/luci-static/resources/view/podkop/**/*.js',
|
||||||
|
], {
|
||||||
|
ignore: [
|
||||||
|
'**/*.test.ts',
|
||||||
|
'**/main.js',
|
||||||
|
'../luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js',
|
||||||
|
],
|
||||||
|
absolute: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const contentRaw = await fs.readFile(file, 'utf8');
|
||||||
|
const content = stripIllegalReturn(contentRaw);
|
||||||
|
const relativePath = path.relative(process.cwd(), file);
|
||||||
|
|
||||||
|
let ast;
|
||||||
|
try {
|
||||||
|
ast = parse(content, {
|
||||||
|
sourceType: 'module',
|
||||||
|
plugins: file.endsWith('.ts') ? ['typescript'] : [],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`⚠️ Parse error in ${relativePath}, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse.default(ast, {
|
||||||
|
CallExpression(path) {
|
||||||
|
if (t.isIdentifier(path.node.callee, { name: '_' })) {
|
||||||
|
const arg = path.node.arguments[0];
|
||||||
|
if (t.isStringLiteral(arg)) {
|
||||||
|
const key = arg.value.trim();
|
||||||
|
if (!key) return; // ❌ пропустить пустые ключи
|
||||||
|
const location = `${relativePath}:${path.node.loc?.start.line ?? '?'}`;
|
||||||
|
|
||||||
|
if (!results[key]) {
|
||||||
|
results[key] = { call: key, key, places: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
results[key].places.push(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const outFile = 'locales/calls.json';
|
||||||
|
const sorted = Object.values(results).sort((a, b) => a.key.localeCompare(b.key)); // 🔤 сортировка по ключу
|
||||||
|
|
||||||
|
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
||||||
|
await fs.writeFile(outFile, JSON.stringify(sorted, null, 2), 'utf8');
|
||||||
|
console.log(`✅ Extracted ${sorted.length} translations to ${outFile}`);
|
||||||
113
fe-app-podkop/generate-po.js
Normal file
113
fe-app-podkop/generate-po.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
const lang = process.argv[2];
|
||||||
|
if (!lang) {
|
||||||
|
console.error('❌ Укажи язык, например: node generate-po.js ru');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const callsPath = 'locales/calls.json';
|
||||||
|
const poPath = `locales/podkop.${lang}.po`;
|
||||||
|
|
||||||
|
function getGitUser() {
|
||||||
|
try {
|
||||||
|
return execSync('git config user.name').toString().trim();
|
||||||
|
} catch {
|
||||||
|
return 'Automatically generated';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeader(lang) {
|
||||||
|
const now = new Date();
|
||||||
|
const date = now.toISOString().split('T')[0];
|
||||||
|
const time = now.toTimeString().split(' ')[0].slice(0, 5);
|
||||||
|
const tzOffset = (() => {
|
||||||
|
const offset = -now.getTimezoneOffset();
|
||||||
|
const sign = offset >= 0 ? '+' : '-';
|
||||||
|
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
||||||
|
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
|
||||||
|
return `${sign}${hours}${minutes}`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const translator = getGitUser();
|
||||||
|
const pluralForms = lang === 'ru'
|
||||||
|
? 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);'
|
||||||
|
: 'nplurals=2; plural=(n != 1);';
|
||||||
|
|
||||||
|
return [
|
||||||
|
`# ${lang.toUpperCase()} translations for PODKOP package.`,
|
||||||
|
`# Copyright (C) ${now.getFullYear()} THE PODKOP'S COPYRIGHT HOLDER`,
|
||||||
|
`# This file is distributed under the same license as the PODKOP package.`,
|
||||||
|
`# ${translator}, ${now.getFullYear()}.`,
|
||||||
|
'#',
|
||||||
|
'msgid ""',
|
||||||
|
'msgstr ""',
|
||||||
|
`"Project-Id-Version: PODKOP\\n"`,
|
||||||
|
`"Report-Msgid-Bugs-To: \\n"`,
|
||||||
|
`"POT-Creation-Date: ${date} ${time}${tzOffset}\\n"`,
|
||||||
|
`"PO-Revision-Date: ${date} ${time}${tzOffset}\\n"`,
|
||||||
|
`"Last-Translator: ${translator}\\n"`,
|
||||||
|
`"Language-Team: none\\n"`,
|
||||||
|
`"Language: ${lang}\\n"`,
|
||||||
|
`"MIME-Version: 1.0\\n"`,
|
||||||
|
`"Content-Type: text/plain; charset=UTF-8\\n"`,
|
||||||
|
`"Content-Transfer-Encoding: 8bit\\n"`,
|
||||||
|
`"Plural-Forms: ${pluralForms}\\n"`,
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePo(content) {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const translations = new Map();
|
||||||
|
let msgid = null;
|
||||||
|
let msgstr = null;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('msgid ')) {
|
||||||
|
msgid = JSON.parse(line.slice(6));
|
||||||
|
} else if (line.startsWith('msgstr ') && msgid !== null) {
|
||||||
|
msgstr = JSON.parse(line.slice(7));
|
||||||
|
translations.set(msgid, msgstr);
|
||||||
|
msgid = null;
|
||||||
|
msgstr = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapePoString(str) {
|
||||||
|
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generatePo() {
|
||||||
|
const [callsRaw, oldPoRaw] = await Promise.all([
|
||||||
|
fs.readFile(callsPath, 'utf8'),
|
||||||
|
fs.readFile(poPath, 'utf8').catch(() => ''),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const calls = JSON.parse(callsRaw);
|
||||||
|
const oldTranslations = parsePo(oldPoRaw);
|
||||||
|
const header = getHeader(lang);
|
||||||
|
|
||||||
|
const body = calls
|
||||||
|
.map(({ key }) => {
|
||||||
|
const msgid = key;
|
||||||
|
const msgstr = oldTranslations.get(msgid) || '';
|
||||||
|
return [
|
||||||
|
`msgid "${escapePoString(msgid)}"`,
|
||||||
|
`msgstr "${escapePoString(msgstr)}"`,
|
||||||
|
''
|
||||||
|
].join('\n');
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const finalPo = header.join('\n') + '\n' + body;
|
||||||
|
|
||||||
|
await fs.writeFile(poPath, finalPo, 'utf8');
|
||||||
|
console.log(`✅ Файл ${poPath} успешно сгенерирован. Переведено ${[...oldTranslations.keys()].length}/${calls.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePo().catch((err) => {
|
||||||
|
console.error('Ошибка генерации PO файла:', err);
|
||||||
|
});
|
||||||
73
fe-app-podkop/generate-pot.js
Normal file
73
fe-app-podkop/generate-pot.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
const inputFile = 'locales/calls.json';
|
||||||
|
const outputFile = 'locales/podkop.pot';
|
||||||
|
const projectId = 'PODKOP';
|
||||||
|
|
||||||
|
function getGitUser() {
|
||||||
|
const name = execSync('git config user.name').toString().trim();
|
||||||
|
const email = execSync('git config user.email').toString().trim();
|
||||||
|
return { name, email };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPotHeader({ name, email }) {
|
||||||
|
const now = new Date();
|
||||||
|
const date = now.toISOString().replace('T', ' ').slice(0, 16);
|
||||||
|
const offset = -now.getTimezoneOffset();
|
||||||
|
const sign = offset >= 0 ? '+' : '-';
|
||||||
|
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
||||||
|
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
|
||||||
|
const timezone = `${sign}${hours}${minutes}`;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'# SOME DESCRIPTIVE TITLE.',
|
||||||
|
`# Copyright (C) ${now.getFullYear()} THE PACKAGE'S COPYRIGHT HOLDER`,
|
||||||
|
`# This file is distributed under the same license as the ${projectId} package.`,
|
||||||
|
`# ${name} <${email}>, ${now.getFullYear()}.`,
|
||||||
|
'#, fuzzy',
|
||||||
|
'msgid ""',
|
||||||
|
'msgstr ""',
|
||||||
|
`"Project-Id-Version: ${projectId}\\n"`,
|
||||||
|
`"Report-Msgid-Bugs-To: \\n"`,
|
||||||
|
`"POT-Creation-Date: ${date}${timezone}\\n"`,
|
||||||
|
`"PO-Revision-Date: ${date}${timezone}\\n"`,
|
||||||
|
`"Last-Translator: ${name} <${email}>\\n"`,
|
||||||
|
`"Language-Team: LANGUAGE <LL@li.org>\\n"`,
|
||||||
|
`"Language: \\n"`,
|
||||||
|
`"MIME-Version: 1.0\\n"`,
|
||||||
|
`"Content-Type: text/plain; charset=UTF-8\\n"`,
|
||||||
|
`"Content-Transfer-Encoding: 8bit\\n"`,
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapePoString(str) {
|
||||||
|
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEntry(item) {
|
||||||
|
const locations = item.places.map(loc => `#: ${loc}`).join('\n');
|
||||||
|
const msgid = escapePoString(item.key);
|
||||||
|
return [
|
||||||
|
locations,
|
||||||
|
`msgid "${msgid}"`,
|
||||||
|
`msgstr ""`,
|
||||||
|
''
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generatePot() {
|
||||||
|
const gitUser = getGitUser();
|
||||||
|
const raw = await fs.readFile(inputFile, 'utf8');
|
||||||
|
const entries = JSON.parse(raw);
|
||||||
|
|
||||||
|
const header = getPotHeader(gitUser);
|
||||||
|
const body = entries.map(generateEntry).join('\n');
|
||||||
|
|
||||||
|
await fs.writeFile(outputFile, `${header}\n${body}`, 'utf8');
|
||||||
|
|
||||||
|
console.log(`✅ POT-файл успешно создан: ${outputFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePot().catch(console.error);
|
||||||
1740
fe-app-podkop/locales/calls.json
Normal file
1740
fe-app-podkop/locales/calls.json
Normal file
File diff suppressed because it is too large
Load Diff
1035
fe-app-podkop/locales/podkop.pot
Normal file
1035
fe-app-podkop/locales/podkop.pot
Normal file
File diff suppressed because it is too large
Load Diff
738
fe-app-podkop/locales/podkop.ru.po
Normal file
738
fe-app-podkop/locales/podkop.ru.po
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
# RU translations for PODKOP package.
|
||||||
|
# Copyright (C) 2025 THE PODKOP'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PODKOP package.
|
||||||
|
# divocat, 2025.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PODKOP\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-11-06 16:19+0200\n"
|
||||||
|
"PO-Revision-Date: 2025-11-06 16:19+0200\n"
|
||||||
|
"Last-Translator: divocat\n"
|
||||||
|
"Language-Team: none\n"
|
||||||
|
"Language: ru\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
|
|
||||||
|
msgid "✔ Enabled"
|
||||||
|
msgstr "✔ Включено"
|
||||||
|
|
||||||
|
msgid "✔ Running"
|
||||||
|
msgstr "✔ Работает"
|
||||||
|
|
||||||
|
msgid "✘ Disabled"
|
||||||
|
msgstr "✘ Отключено"
|
||||||
|
|
||||||
|
msgid "✘ Stopped"
|
||||||
|
msgstr "✘ Остановлен"
|
||||||
|
|
||||||
|
msgid "Active Connections"
|
||||||
|
msgstr "Активные соединения"
|
||||||
|
|
||||||
|
msgid "Additional marking rules found"
|
||||||
|
msgstr "Найдены дополнительные правила маркировки"
|
||||||
|
|
||||||
|
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
|
||||||
|
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
|
||||||
|
|
||||||
|
msgid "Applicable for SOCKS and Shadowsocks proxy"
|
||||||
|
msgstr "Применимо для SOCKS и Shadowsocks прокси"
|
||||||
|
|
||||||
|
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
|
||||||
|
msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
|
||||||
|
|
||||||
|
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
|
||||||
|
msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы."
|
||||||
|
|
||||||
|
msgid "Available actions"
|
||||||
|
msgstr "Доступные действия"
|
||||||
|
|
||||||
|
msgid "Bootsrap DNS"
|
||||||
|
msgstr "Bootstrap DNS"
|
||||||
|
|
||||||
|
msgid "Bootstrap DNS server"
|
||||||
|
msgstr "Bootstrap DNS-сервер"
|
||||||
|
|
||||||
|
msgid "Browser is not using FakeIP"
|
||||||
|
msgstr "Браузер не использует FakeIP"
|
||||||
|
|
||||||
|
msgid "Browser is using FakeIP correctly"
|
||||||
|
msgstr "Браузер использует FakeIP"
|
||||||
|
|
||||||
|
msgid "Cache File Path"
|
||||||
|
msgstr "Путь к файлу кэша"
|
||||||
|
|
||||||
|
msgid "Cache file path cannot be empty"
|
||||||
|
msgstr "Путь к файлу кэша не может быть пустым"
|
||||||
|
|
||||||
|
msgid "Cannot receive checks result"
|
||||||
|
msgstr "Не удалось получить результаты проверки"
|
||||||
|
|
||||||
|
msgid "Checking, please wait"
|
||||||
|
msgstr "Проверяем, пожалуйста подождите"
|
||||||
|
|
||||||
|
msgid "checks"
|
||||||
|
msgstr "проверки"
|
||||||
|
|
||||||
|
msgid "Checks failed"
|
||||||
|
msgstr "Проверки не выполнены"
|
||||||
|
|
||||||
|
msgid "Checks passed"
|
||||||
|
msgstr "Проверки пройдены"
|
||||||
|
|
||||||
|
msgid "CIDR must be between 0 and 32"
|
||||||
|
msgstr "CIDR должен быть между 0 и 32"
|
||||||
|
|
||||||
|
msgid "Close"
|
||||||
|
msgstr "Закрыть"
|
||||||
|
|
||||||
|
msgid "Community Lists"
|
||||||
|
msgstr "Списки сообщества"
|
||||||
|
|
||||||
|
msgid "Config File Path"
|
||||||
|
msgstr "Путь к файлу конфигурации"
|
||||||
|
|
||||||
|
msgid "Configuration for Podkop service"
|
||||||
|
msgstr "Настройки сервиса Podkop"
|
||||||
|
|
||||||
|
msgid "Configuration Type"
|
||||||
|
msgstr "Тип конфигурации"
|
||||||
|
|
||||||
|
msgid "Connection Type"
|
||||||
|
msgstr "Тип подключения"
|
||||||
|
|
||||||
|
msgid "Connection URL"
|
||||||
|
msgstr "URL подключения"
|
||||||
|
|
||||||
|
msgid "Copy"
|
||||||
|
msgstr "Копировать"
|
||||||
|
|
||||||
|
msgid "Currently unavailable"
|
||||||
|
msgstr "Временно недоступно"
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr "Дашборд"
|
||||||
|
|
||||||
|
msgid "Dashboard currently unavailable"
|
||||||
|
msgstr "Дашборд сейчас недоступен"
|
||||||
|
|
||||||
|
msgid "Delay in milliseconds before reloading podkop after interface UP"
|
||||||
|
msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса"
|
||||||
|
|
||||||
|
msgid "Delay value cannot be empty"
|
||||||
|
msgstr "Значение задержки не может быть пустым"
|
||||||
|
|
||||||
|
msgid "DHCP has DNS server"
|
||||||
|
msgstr "DHCP содержит DNS сервер"
|
||||||
|
|
||||||
|
msgid "Diagnostics"
|
||||||
|
msgstr "Диагностика"
|
||||||
|
|
||||||
|
msgid "Disable autostart"
|
||||||
|
msgstr "Отключить автостарт"
|
||||||
|
|
||||||
|
msgid "Disable QUIC"
|
||||||
|
msgstr "Отключить QUIC"
|
||||||
|
|
||||||
|
msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming"
|
||||||
|
msgstr "Отключить QUIC протокол для улучшения совместимости или исправления видео стриминга"
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr "Отключено"
|
||||||
|
|
||||||
|
msgid "DNS on router"
|
||||||
|
msgstr "DNS на роутере"
|
||||||
|
|
||||||
|
msgid "DNS over HTTPS (DoH)"
|
||||||
|
msgstr "DNS через HTTPS (DoH)"
|
||||||
|
|
||||||
|
msgid "DNS over TLS (DoT)"
|
||||||
|
msgstr "DNS через TLS (DoT)"
|
||||||
|
|
||||||
|
msgid "DNS Protocol Type"
|
||||||
|
msgstr "Тип протокола DNS"
|
||||||
|
|
||||||
|
msgid "DNS Rewrite TTL"
|
||||||
|
msgstr "Перезапись TTL для DNS"
|
||||||
|
|
||||||
|
msgid "DNS Server"
|
||||||
|
msgstr "DNS-сервер"
|
||||||
|
|
||||||
|
msgid "DNS server address cannot be empty"
|
||||||
|
msgstr "Адрес DNS-сервера не может быть пустым"
|
||||||
|
|
||||||
|
msgid "Do not panic, everything can be fixed, just..."
|
||||||
|
msgstr "Не паникуйте, всё можно исправить, просто..."
|
||||||
|
|
||||||
|
msgid "Domain Resolver"
|
||||||
|
msgstr "Резолвер доменов"
|
||||||
|
|
||||||
|
msgid "Dont Touch My DHCP!"
|
||||||
|
msgstr "Dont Touch My DHCP!"
|
||||||
|
|
||||||
|
msgid "Downlink"
|
||||||
|
msgstr "Входящий"
|
||||||
|
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Скачать"
|
||||||
|
|
||||||
|
msgid "Download Lists via Proxy/VPN"
|
||||||
|
msgstr "Скачивать списки через Proxy/VPN"
|
||||||
|
|
||||||
|
msgid "Download Lists via specific proxy section"
|
||||||
|
msgstr "Скачивать списки через выбранную секцию"
|
||||||
|
|
||||||
|
msgid "Downloading all lists via specific Proxy/VPN"
|
||||||
|
msgstr "Загрузка всех списков через указанный прокси/VPN"
|
||||||
|
|
||||||
|
msgid "Dynamic List"
|
||||||
|
msgstr "Динамический список"
|
||||||
|
|
||||||
|
msgid "Enable autostart"
|
||||||
|
msgstr "Включить автостарт"
|
||||||
|
|
||||||
|
msgid "Enable built-in DNS resolver for domains handled by this section"
|
||||||
|
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе"
|
||||||
|
|
||||||
|
msgid "Enable Mixed Proxy"
|
||||||
|
msgstr "Включить смешанный прокси"
|
||||||
|
|
||||||
|
msgid "Enable Output Network Interface"
|
||||||
|
msgstr "Включить выходной сетевой интерфейс"
|
||||||
|
|
||||||
|
msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies"
|
||||||
|
msgstr "Включить смешанный прокси-сервер, разрешив этому разделу маршрутизировать трафик как через HTTP, так и через SOCKS-прокси."
|
||||||
|
|
||||||
|
msgid "Enable YACD"
|
||||||
|
msgstr "Включить YACD"
|
||||||
|
|
||||||
|
msgid "Enable YACD WAN Access"
|
||||||
|
msgstr "Включить доступ YACD WAN"
|
||||||
|
|
||||||
|
msgid "Enter complete outbound configuration in JSON format"
|
||||||
|
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
|
||||||
|
|
||||||
|
msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //"
|
||||||
|
msgstr "Введите доменные имена, разделяя их запятыми, пробелами или переносами строк. Вы можете добавлять комментарии, используя //"
|
||||||
|
|
||||||
|
msgid "Enter domain names without protocols, e.g. example.com or sub.example.com"
|
||||||
|
msgstr "Введите доменные имена без протоколов, например example.com или sub.example.com"
|
||||||
|
|
||||||
|
msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses"
|
||||||
|
msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса"
|
||||||
|
|
||||||
|
msgid "Every 1 minute"
|
||||||
|
msgstr "Каждую минуту"
|
||||||
|
|
||||||
|
msgid "Every 3 minutes"
|
||||||
|
msgstr "Каждые 3 минуты"
|
||||||
|
|
||||||
|
msgid "Every 30 seconds"
|
||||||
|
msgstr "Каждые 30 секунд"
|
||||||
|
|
||||||
|
msgid "Every 5 minutes"
|
||||||
|
msgstr "Каждые 5 минут"
|
||||||
|
|
||||||
|
msgid "Exclude NTP"
|
||||||
|
msgstr "Исключить NTP"
|
||||||
|
|
||||||
|
msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN"
|
||||||
|
msgstr "Исключите трафик протокола NTP из туннеля, чтобы предотвратить его маршрутизацию через прокси-сервер или VPN."
|
||||||
|
|
||||||
|
msgid "Failed to copy!"
|
||||||
|
msgstr "Не удалось скопировать!"
|
||||||
|
|
||||||
|
msgid "Failed to execute!"
|
||||||
|
msgstr "Не удалось выполнить!"
|
||||||
|
|
||||||
|
msgid "Fastest"
|
||||||
|
msgstr "Самый быстрый"
|
||||||
|
|
||||||
|
msgid "Fully Routed IPs"
|
||||||
|
msgstr "Полностью маршрутизированные IP-адреса"
|
||||||
|
|
||||||
|
msgid "Get global check"
|
||||||
|
msgstr "Получить глобальную проверку"
|
||||||
|
|
||||||
|
msgid "Global check"
|
||||||
|
msgstr "Глобальная проверка"
|
||||||
|
|
||||||
|
msgid "HTTP error"
|
||||||
|
msgstr "Ошибка HTTP"
|
||||||
|
|
||||||
|
msgid "Interface Monitoring"
|
||||||
|
msgstr "Мониторинг интерфейса"
|
||||||
|
|
||||||
|
msgid "Interface Monitoring Delay"
|
||||||
|
msgstr "Задержка при мониторинге интерфейсов"
|
||||||
|
|
||||||
|
msgid "Interface monitoring for Bad WAN"
|
||||||
|
msgstr "Мониторинг интерфейса для Bad WAN"
|
||||||
|
|
||||||
|
msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH"
|
||||||
|
msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH"
|
||||||
|
|
||||||
|
msgid "Invalid domain address"
|
||||||
|
msgstr "Неверный домен"
|
||||||
|
|
||||||
|
msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y"
|
||||||
|
msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y"
|
||||||
|
|
||||||
|
msgid "Invalid IP address"
|
||||||
|
msgstr "Неверный IP-адрес"
|
||||||
|
|
||||||
|
msgid "Invalid JSON format"
|
||||||
|
msgstr "Неверный формат JSON"
|
||||||
|
|
||||||
|
msgid "Invalid path format. Path must start with \"/\" and contain valid characters"
|
||||||
|
msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы"
|
||||||
|
|
||||||
|
msgid "Invalid port number. Must be between 1 and 65535"
|
||||||
|
msgstr "Неверный номер порта. Допустимо от 1 до 65535"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password"
|
||||||
|
msgstr "Неверный URL Shadowsocks: декодированные данные должны содержать method:password"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing credentials"
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing method and password separator \":\""
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing port"
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствует порт"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing server"
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствует сервер"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing server address"
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: must not contain spaces"
|
||||||
|
msgstr "Неверный URL Shadowsocks: не должен содержать пробелов"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: must start with ss://"
|
||||||
|
msgstr "Неверный URL Shadowsocks: должен начинаться с ss://"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: parsing failed"
|
||||||
|
msgstr "Неверный URL Shadowsocks: ошибка разбора"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: invalid host format"
|
||||||
|
msgstr "Неверный URL SOCKS: неверный формат хоста"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: invalid port number"
|
||||||
|
msgstr "Неверный URL SOCKS: неверный номер порта"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: missing host and port"
|
||||||
|
msgstr "Неверный URL SOCKS: отсутствует хост и порт"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: missing hostname or IP"
|
||||||
|
msgstr "Неверный URL SOCKS: отсутствует имя хоста или IP-адрес"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: missing port"
|
||||||
|
msgstr "Неверный URL SOCKS: отсутствует порт"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: missing username"
|
||||||
|
msgstr "Неверный URL SOCKS: отсутствует имя пользователя"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: must not contain spaces"
|
||||||
|
msgstr "Неверный URL SOCKS: не должен содержать пробелов"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://"
|
||||||
|
msgstr "Неверный URL-адрес SOCKS: должен начинаться с socks4://, socks4a:// или socks5://"
|
||||||
|
|
||||||
|
msgid "Invalid SOCKS URL: parsing failed"
|
||||||
|
msgstr "Неверный URL SOCKS: парсинг не удался"
|
||||||
|
|
||||||
|
msgid "Invalid Trojan URL: must not contain spaces"
|
||||||
|
msgstr "Неверный URL Trojan: не должен содержать пробелов"
|
||||||
|
|
||||||
|
msgid "Invalid Trojan URL: must start with trojan://"
|
||||||
|
msgstr "Неверный URL Trojan: должен начинаться с trojan://"
|
||||||
|
|
||||||
|
msgid "Invalid Trojan URL: parsing failed"
|
||||||
|
msgstr "Неверный URL Trojan: ошибка разбора"
|
||||||
|
|
||||||
|
msgid "Invalid URL format"
|
||||||
|
msgstr "Неверный формат URL"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: parsing failed"
|
||||||
|
msgstr "Неверный URL VLESS: ошибка разбора"
|
||||||
|
|
||||||
|
msgid "IP address 0.0.0.0 is not allowed"
|
||||||
|
msgstr "IP-адрес 0.0.0.0 не допускается"
|
||||||
|
|
||||||
|
msgid "Issues detected"
|
||||||
|
msgstr "Обнаружены проблемы"
|
||||||
|
|
||||||
|
msgid "Latest"
|
||||||
|
msgstr "Последняя"
|
||||||
|
|
||||||
|
msgid "List Update Frequency"
|
||||||
|
msgstr "Частота обновления списков"
|
||||||
|
|
||||||
|
msgid "Local Domain Lists"
|
||||||
|
msgstr "Локальные списки доменов"
|
||||||
|
|
||||||
|
msgid "Local Subnet Lists"
|
||||||
|
msgstr "Локальные списки подсетей"
|
||||||
|
|
||||||
|
msgid "Main DNS"
|
||||||
|
msgstr "Основной DNS"
|
||||||
|
|
||||||
|
msgid "Memory Usage"
|
||||||
|
msgstr "Использование памяти"
|
||||||
|
|
||||||
|
msgid "Mixed Proxy Port"
|
||||||
|
msgstr "Порт смешанного прокси"
|
||||||
|
|
||||||
|
msgid "Monitored Interfaces"
|
||||||
|
msgstr "Наблюдаемые интерфейсы"
|
||||||
|
|
||||||
|
msgid "Must be a number in the range of 50 - 1000"
|
||||||
|
msgstr "Должно быть числом от 50 до 1000"
|
||||||
|
|
||||||
|
msgid "Network Interface"
|
||||||
|
msgstr "Сетевой интерфейс"
|
||||||
|
|
||||||
|
msgid "No other marking rules found"
|
||||||
|
msgstr "Другие правила маркировки не найдены"
|
||||||
|
|
||||||
|
msgid "Not implement yet"
|
||||||
|
msgstr "Ещё не реализовано"
|
||||||
|
|
||||||
|
msgid "Not responding"
|
||||||
|
msgstr "Не отвечает"
|
||||||
|
|
||||||
|
msgid "Not running"
|
||||||
|
msgstr "Не запущено"
|
||||||
|
|
||||||
|
msgid "Operation timed out"
|
||||||
|
msgstr "Время ожидания истекло"
|
||||||
|
|
||||||
|
msgid "Outbound Config"
|
||||||
|
msgstr "Конфигурация Outbound"
|
||||||
|
|
||||||
|
msgid "Outbound Configuration"
|
||||||
|
msgstr "Конфигурация исходящего соединения"
|
||||||
|
|
||||||
|
msgid "Outdated"
|
||||||
|
msgstr "Устаревшая"
|
||||||
|
|
||||||
|
msgid "Output Network Interface"
|
||||||
|
msgstr "Выходной сетевой интерфейс"
|
||||||
|
|
||||||
|
msgid "Path cannot be empty"
|
||||||
|
msgstr "Путь не может быть пустым"
|
||||||
|
|
||||||
|
msgid "Path must be absolute (start with /)"
|
||||||
|
msgstr "Путь должен быть абсолютным (начинаться с /)"
|
||||||
|
|
||||||
|
msgid "Path must contain at least one directory (like /tmp/cache.db)"
|
||||||
|
msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)"
|
||||||
|
|
||||||
|
msgid "Path must end with cache.db"
|
||||||
|
msgstr "Путь должен заканчиваться на cache.db"
|
||||||
|
|
||||||
|
msgid "Pending"
|
||||||
|
msgstr "Ожидает запуска"
|
||||||
|
|
||||||
|
msgid "Podkop"
|
||||||
|
msgstr "Podkop"
|
||||||
|
|
||||||
|
msgid "Podkop Settings"
|
||||||
|
msgstr "Настройки podkop"
|
||||||
|
|
||||||
|
msgid "Podkop will not modify your DHCP configuration"
|
||||||
|
msgstr "Podkop не будет изменять вашу конфигурацию DHCP."
|
||||||
|
|
||||||
|
msgid "Proxy Configuration URL"
|
||||||
|
msgstr "URL конфигурации прокси"
|
||||||
|
|
||||||
|
msgid "Proxy traffic is not routed via FakeIP"
|
||||||
|
msgstr "Прокси-трафик не маршрутизируется через FakeIP"
|
||||||
|
|
||||||
|
msgid "Proxy traffic is routed via FakeIP"
|
||||||
|
msgstr "Прокси-трафик направляется через FakeIP"
|
||||||
|
|
||||||
|
msgid "Regional options cannot be used together"
|
||||||
|
msgstr "Нельзя использовать несколько региональных опций одновременно"
|
||||||
|
|
||||||
|
msgid "Remote Domain Lists"
|
||||||
|
msgstr "Внешние списки доменов"
|
||||||
|
|
||||||
|
msgid "Remote Subnet Lists"
|
||||||
|
msgstr "Внешние списки подсетей"
|
||||||
|
|
||||||
|
msgid "Restart podkop"
|
||||||
|
msgstr "Перезапустить Podkop"
|
||||||
|
|
||||||
|
msgid "Router DNS is not routed through sing-box"
|
||||||
|
msgstr "DNS роутера не проходит через sing-box"
|
||||||
|
|
||||||
|
msgid "Router DNS is routed through sing-box"
|
||||||
|
msgstr "DNS роутера проходит через sing-box"
|
||||||
|
|
||||||
|
msgid "Routing Excluded IPs"
|
||||||
|
msgstr "Исключённые из маршрутизации IP-адреса"
|
||||||
|
|
||||||
|
msgid "Rules mangle counters"
|
||||||
|
msgstr "Счётчики правил mangle"
|
||||||
|
|
||||||
|
msgid "Rules mangle exist"
|
||||||
|
msgstr "Правила mangle существуют"
|
||||||
|
|
||||||
|
msgid "Rules mangle output counters"
|
||||||
|
msgstr "Счётчики правил mangle output"
|
||||||
|
|
||||||
|
msgid "Rules mangle output exist"
|
||||||
|
msgstr "Правила mangle output существуют"
|
||||||
|
|
||||||
|
msgid "Rules proxy counters"
|
||||||
|
msgstr "Счётчики правил proxy"
|
||||||
|
|
||||||
|
msgid "Rules proxy exist"
|
||||||
|
msgstr "Правила прокси существуют"
|
||||||
|
|
||||||
|
msgid "Run Diagnostic"
|
||||||
|
msgstr "Запустить диагностику"
|
||||||
|
|
||||||
|
msgid "Russia inside restrictions"
|
||||||
|
msgstr "Ограничения Russia inside"
|
||||||
|
|
||||||
|
msgid "Secret key for authenticating remote access to YACD when WAN access is enabled."
|
||||||
|
msgstr "Секретный ключ для аутентификации удаленного доступа к YACD при включенном доступе через WAN."
|
||||||
|
|
||||||
|
msgid "Sections"
|
||||||
|
msgstr "Секции"
|
||||||
|
|
||||||
|
msgid "Select a predefined list for routing"
|
||||||
|
msgstr "Выберите предопределенный список для маршрутизации"
|
||||||
|
|
||||||
|
msgid "Select between VPN and Proxy connection methods for traffic routing"
|
||||||
|
msgstr "Выберите между VPN и Proxy методами для маршрутизации трафика"
|
||||||
|
|
||||||
|
msgid "Select DNS protocol to use"
|
||||||
|
msgstr "Выберите протокол DNS"
|
||||||
|
|
||||||
|
msgid "Select how often the domain or subnet lists are updated automatically"
|
||||||
|
msgstr "Выберите частоту автоматического обновления списков доменов или подсетей."
|
||||||
|
|
||||||
|
msgid "Select how to configure the proxy"
|
||||||
|
msgstr "Выберите способ настройки прокси"
|
||||||
|
|
||||||
|
msgid "Select network interface for VPN connection"
|
||||||
|
msgstr "Выберите сетевой интерфейс для VPN подключения"
|
||||||
|
|
||||||
|
msgid "Select or enter DNS server address"
|
||||||
|
msgstr "Выберите или введите адрес DNS-сервера"
|
||||||
|
|
||||||
|
msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing"
|
||||||
|
msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
|
||||||
|
|
||||||
|
msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing"
|
||||||
|
msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
|
||||||
|
|
||||||
|
msgid "Select the DNS protocol type for the domain resolver"
|
||||||
|
msgstr "Выберите тип протокола DNS для резолвера доменов"
|
||||||
|
|
||||||
|
msgid "Select the list type for adding custom domains"
|
||||||
|
msgstr "Выберите тип списка для добавления пользовательских доменов"
|
||||||
|
|
||||||
|
msgid "Select the list type for adding custom subnets"
|
||||||
|
msgstr "Выберите тип списка для добавления пользовательских подсетей"
|
||||||
|
|
||||||
|
msgid "Select the network interface from which the traffic will originate"
|
||||||
|
msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик"
|
||||||
|
|
||||||
|
msgid "Select the network interface to which the traffic will originate"
|
||||||
|
msgstr "Выберите сетевой интерфейс, на который будет поступать трафик."
|
||||||
|
|
||||||
|
msgid "Select the WAN interfaces to be monitored"
|
||||||
|
msgstr "Выберите WAN интерфейсы для мониторинга"
|
||||||
|
|
||||||
|
msgid "Services info"
|
||||||
|
msgstr "Информация о сервисах"
|
||||||
|
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr "Настройки"
|
||||||
|
|
||||||
|
msgid "Show sing-box config"
|
||||||
|
msgstr "Показать sing-box конфигурацию"
|
||||||
|
|
||||||
|
msgid "Sing-box"
|
||||||
|
msgstr "Sing-box"
|
||||||
|
|
||||||
|
msgid "Sing-box autostart disabled"
|
||||||
|
msgstr "Автостарт sing-box отключен"
|
||||||
|
|
||||||
|
msgid "Sing-box installed"
|
||||||
|
msgstr "Sing-box установлен"
|
||||||
|
|
||||||
|
msgid "Sing-box listening ports"
|
||||||
|
msgstr "Sing-box слушает порты"
|
||||||
|
|
||||||
|
msgid "Sing-box process running"
|
||||||
|
msgstr "Процесс sing-box запущен"
|
||||||
|
|
||||||
|
msgid "Sing-box service exist"
|
||||||
|
msgstr "Сервис sing-box существует"
|
||||||
|
|
||||||
|
msgid "Sing-box version is compatible (newer than 1.12.4)"
|
||||||
|
msgstr "Версия Sing-box совместима (новее 1.12.4)"
|
||||||
|
|
||||||
|
msgid "Source Network Interface"
|
||||||
|
msgstr "Сетевой интерфейс источника"
|
||||||
|
|
||||||
|
msgid "Specify a local IP address to be excluded from routing"
|
||||||
|
msgstr "Укажите локальный IP-адрес, который следует исключить из маршрутизации."
|
||||||
|
|
||||||
|
msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route"
|
||||||
|
msgstr "Укажите локальные IP-адреса или подсети, трафик которых всегда будет направляться через настроенный маршрут."
|
||||||
|
|
||||||
|
msgid "Specify remote URLs to download and use domain lists"
|
||||||
|
msgstr "Укажите URL-адреса для загрузки и использования списков доменов."
|
||||||
|
|
||||||
|
msgid "Specify remote URLs to download and use subnet lists"
|
||||||
|
msgstr "Укажите URL-адреса для загрузки и использования списков подсетей."
|
||||||
|
|
||||||
|
msgid "Specify the path to the list file located on the router filesystem"
|
||||||
|
msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора."
|
||||||
|
|
||||||
|
msgid "Start podkop"
|
||||||
|
msgstr "Запустить podkop"
|
||||||
|
|
||||||
|
msgid "Stop podkop"
|
||||||
|
msgstr "Остановить podkop"
|
||||||
|
|
||||||
|
msgid "Successfully copied!"
|
||||||
|
msgstr "Успешно скопировано!"
|
||||||
|
|
||||||
|
msgid "System info"
|
||||||
|
msgstr "Системная информация"
|
||||||
|
|
||||||
|
msgid "System information"
|
||||||
|
msgstr "Системная информация"
|
||||||
|
|
||||||
|
msgid "Table exist"
|
||||||
|
msgstr "Таблица существует"
|
||||||
|
|
||||||
|
msgid "Test latency"
|
||||||
|
msgstr "Тестирование задержки"
|
||||||
|
|
||||||
|
msgid "Text List"
|
||||||
|
msgstr "Текстовый список"
|
||||||
|
|
||||||
|
msgid "Text List (comma/space/newline separated)"
|
||||||
|
msgstr "Текстовый список (через запятую, пробел или новую строку)"
|
||||||
|
|
||||||
|
msgid "The DNS server used to look up the IP address of an upstream DNS server"
|
||||||
|
msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера"
|
||||||
|
|
||||||
|
msgid "The interval between connectivity tests"
|
||||||
|
msgstr "Интервал между тестами подключения"
|
||||||
|
|
||||||
|
msgid "The maximum difference in response times (ms) allowed when comparing servers"
|
||||||
|
msgstr "Максимально допустимая разница во времени отклика (мс) при сравнении серверов"
|
||||||
|
|
||||||
|
msgid "The URL used to test server connectivity"
|
||||||
|
msgstr "URL-адрес, используемый для проверки подключения к серверу"
|
||||||
|
|
||||||
|
msgid "Time in seconds for DNS record caching (default: 60)"
|
||||||
|
msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)"
|
||||||
|
|
||||||
|
msgid "Traffic"
|
||||||
|
msgstr "Трафик"
|
||||||
|
|
||||||
|
msgid "Traffic Total"
|
||||||
|
msgstr "Всего трафика"
|
||||||
|
|
||||||
|
msgid "Troubleshooting"
|
||||||
|
msgstr "Устранение неполадок"
|
||||||
|
|
||||||
|
msgid "TTL must be a positive number"
|
||||||
|
msgstr "TTL должно быть положительным числом"
|
||||||
|
|
||||||
|
msgid "TTL value cannot be empty"
|
||||||
|
msgstr "Значение TTL не может быть пустым"
|
||||||
|
|
||||||
|
msgid "UDP (Unprotected DNS)"
|
||||||
|
msgstr "UDP (Незащищённый DNS)"
|
||||||
|
|
||||||
|
msgid "UDP over TCP"
|
||||||
|
msgstr "UDP через TCP"
|
||||||
|
|
||||||
|
msgid "unknown"
|
||||||
|
msgstr "неизвестно"
|
||||||
|
|
||||||
|
msgid "Unknown error"
|
||||||
|
msgstr "Неизвестная ошибка"
|
||||||
|
|
||||||
|
msgid "Uplink"
|
||||||
|
msgstr "Исходящий"
|
||||||
|
|
||||||
|
msgid "URL must start with vless://, ss://, trojan://, or socks4/5://"
|
||||||
|
msgstr "URL должен начинаться с vless://, ss://, trojan:// или socks4/5://"
|
||||||
|
|
||||||
|
msgid "URL must use one of the following protocols:"
|
||||||
|
msgstr "URL должен использовать один из следующих протоколов:"
|
||||||
|
|
||||||
|
msgid "URLTest"
|
||||||
|
msgstr "URLTest"
|
||||||
|
|
||||||
|
msgid "URLTest Check Interval"
|
||||||
|
msgstr "Интервал проверки URLTest"
|
||||||
|
|
||||||
|
msgid "URLTest Proxy Links"
|
||||||
|
msgstr "Ссылки прокси для URLTest"
|
||||||
|
|
||||||
|
msgid "URLTest Testing URL"
|
||||||
|
msgstr "URLTest ссылка для проверки"
|
||||||
|
|
||||||
|
msgid "URLTest Tolerance"
|
||||||
|
msgstr "URLTest допустимое отклонение"
|
||||||
|
|
||||||
|
msgid "User Domain List Type"
|
||||||
|
msgstr "Тип пользовательского списка доменов"
|
||||||
|
|
||||||
|
msgid "User Domains"
|
||||||
|
msgstr "Пользовательские домены"
|
||||||
|
|
||||||
|
msgid "User Domains List"
|
||||||
|
msgstr "Список пользовательских доменов"
|
||||||
|
|
||||||
|
msgid "User Subnet List Type"
|
||||||
|
msgstr "Тип пользовательского списка подсетей"
|
||||||
|
|
||||||
|
msgid "User Subnets"
|
||||||
|
msgstr "Пользовательские подсети"
|
||||||
|
|
||||||
|
msgid "User Subnets List"
|
||||||
|
msgstr "Список пользовательских подсетей"
|
||||||
|
|
||||||
|
msgid "Valid"
|
||||||
|
msgstr "Валидно"
|
||||||
|
|
||||||
|
msgid "Validation errors:"
|
||||||
|
msgstr "Ошибки валидации:"
|
||||||
|
|
||||||
|
msgid "View logs"
|
||||||
|
msgstr "Посмотреть логи"
|
||||||
|
|
||||||
|
msgid "Visit Wiki"
|
||||||
|
msgstr "Перейти в wiki"
|
||||||
|
|
||||||
|
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
|
||||||
|
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
|
||||||
|
|
||||||
|
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
|
||||||
|
msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных."
|
||||||
|
|
||||||
|
msgid "YACD Secret Key"
|
||||||
|
msgstr "Секретный ключ YACD"
|
||||||
|
|
||||||
|
msgid "You can select Output Network Interface, by default autodetect"
|
||||||
|
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически."
|
||||||
@@ -5,21 +5,30 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
|
"format:js": "prettier --write ../luci-app-podkop/htdocs/luci-static/resources/view/podkop",
|
||||||
"lint": "eslint src --ext .ts,.tsx",
|
"lint": "eslint src --ext .ts,.tsx",
|
||||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||||
"build": "tsup src/main.ts",
|
"build": "tsup src/main.ts",
|
||||||
"dev": "tsup src/main.ts --watch",
|
"dev": "tsup src/main.ts --watch",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build",
|
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build",
|
||||||
"watch:sftp": "node watch-upload.js"
|
"watch:sftp": "node watch-upload.js",
|
||||||
|
"locales:exctract-calls": "node extract-calls.js",
|
||||||
|
"locales:generate-pot": "node generate-pot.js",
|
||||||
|
"locales:generate-po:ru": "node generate-po.js ru",
|
||||||
|
"locales:distribute": "node distribute-locales.js",
|
||||||
|
"locales:actualize": "yarn locales:exctract-calls && yarn locales:generate-pot && yarn locales:generate-po:ru && yarn locales:distribute"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/parser": "7.28.4",
|
||||||
|
"@babel/traverse": "7.28.4",
|
||||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||||
"@typescript-eslint/parser": "8.45.0",
|
"@typescript-eslint/parser": "8.45.0",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"dotenv": "17.2.3",
|
"dotenv": "17.2.3",
|
||||||
"eslint": "9.36.0",
|
"eslint": "9.36.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"fast-glob": "3.3.3",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"ssh2-sftp-client": "12.0.1",
|
"ssh2-sftp-client": "12.0.1",
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './types';
|
|
||||||
export * from './methods';
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { IBaseApiResponse } from '../types';
|
|
||||||
|
|
||||||
export async function createBaseApiRequest<T>(
|
|
||||||
fetchFn: () => Promise<Response>,
|
|
||||||
): Promise<IBaseApiResponse<T>> {
|
|
||||||
try {
|
|
||||||
const response = await fetchFn();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
success: false as const,
|
|
||||||
message: `${_('HTTP error')} ${response.status}: ${response.statusText}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: T = await response.json();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true as const,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
success: false as const,
|
|
||||||
message: e instanceof Error ? e.message : _('Unknown error'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ClashAPI, IBaseApiResponse } from '../types';
|
|
||||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
|
||||||
import { getClashApiUrl } from '../../helpers';
|
|
||||||
|
|
||||||
export async function getClashConfig(): Promise<
|
|
||||||
IBaseApiResponse<ClashAPI.Config>
|
|
||||||
> {
|
|
||||||
return createBaseApiRequest<ClashAPI.Config>(() =>
|
|
||||||
fetch(`${getClashApiUrl()}/configs`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { ClashAPI, IBaseApiResponse } from '../types';
|
|
||||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
|
||||||
import { getClashApiUrl } from '../../helpers';
|
|
||||||
|
|
||||||
export async function getClashGroupDelay(
|
|
||||||
group: string,
|
|
||||||
url = 'https://www.gstatic.com/generate_204',
|
|
||||||
timeout = 2000,
|
|
||||||
): Promise<IBaseApiResponse<ClashAPI.Delays>> {
|
|
||||||
const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent(
|
|
||||||
url,
|
|
||||||
)}&timeout=${timeout}`;
|
|
||||||
|
|
||||||
return createBaseApiRequest<ClashAPI.Delays>(() =>
|
|
||||||
fetch(endpoint, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ClashAPI, IBaseApiResponse } from '../types';
|
|
||||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
|
||||||
import { getClashApiUrl } from '../../helpers';
|
|
||||||
|
|
||||||
export async function getClashProxies(): Promise<
|
|
||||||
IBaseApiResponse<ClashAPI.Proxies>
|
|
||||||
> {
|
|
||||||
return createBaseApiRequest<ClashAPI.Proxies>(() =>
|
|
||||||
fetch(`${getClashApiUrl()}/proxies`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ClashAPI, IBaseApiResponse } from '../types';
|
|
||||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
|
||||||
import { getClashApiUrl } from '../../helpers';
|
|
||||||
|
|
||||||
export async function getClashVersion(): Promise<
|
|
||||||
IBaseApiResponse<ClashAPI.Version>
|
|
||||||
> {
|
|
||||||
return createBaseApiRequest<ClashAPI.Version>(() =>
|
|
||||||
fetch(`${getClashApiUrl()}/version`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export * from './createBaseApiRequest';
|
|
||||||
export * from './getConfig';
|
|
||||||
export * from './getGroupDelay';
|
|
||||||
export * from './getProxies';
|
|
||||||
export * from './getVersion';
|
|
||||||
export * from './triggerProxySelector';
|
|
||||||
export * from './triggerLatencyTest';
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { IBaseApiResponse } from '../types';
|
|
||||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
|
||||||
import { getClashApiUrl } from '../../helpers';
|
|
||||||
|
|
||||||
export async function triggerLatencyGroupTest(
|
|
||||||
tag: string,
|
|
||||||
timeout: number = 5000,
|
|
||||||
url: string = 'https://www.gstatic.com/generate_204',
|
|
||||||
): Promise<IBaseApiResponse<void>> {
|
|
||||||
return createBaseApiRequest<void>(() =>
|
|
||||||
fetch(
|
|
||||||
`${getClashApiUrl()}/group/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function triggerLatencyProxyTest(
|
|
||||||
tag: string,
|
|
||||||
timeout: number = 2000,
|
|
||||||
url: string = 'https://www.gstatic.com/generate_204',
|
|
||||||
): Promise<IBaseApiResponse<void>> {
|
|
||||||
return createBaseApiRequest<void>(() =>
|
|
||||||
fetch(
|
|
||||||
`${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { IBaseApiResponse } from '../types';
|
|
||||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
|
||||||
import { getClashApiUrl } from '../../helpers';
|
|
||||||
|
|
||||||
export async function triggerProxySelector(
|
|
||||||
selector: string,
|
|
||||||
outbound: string,
|
|
||||||
): Promise<IBaseApiResponse<void>> {
|
|
||||||
return createBaseApiRequest<void>(() =>
|
|
||||||
fetch(`${getClashApiUrl()}/proxies/${selector}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name: outbound }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
export type IBaseApiResponse<T> =
|
|
||||||
| {
|
|
||||||
success: true;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
success: false;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
||||||
export namespace ClashAPI {
|
|
||||||
export interface Version {
|
|
||||||
meta: boolean;
|
|
||||||
premium: boolean;
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
port: number;
|
|
||||||
'socks-port': number;
|
|
||||||
'redir-port': number;
|
|
||||||
'tproxy-port': number;
|
|
||||||
'mixed-port': number;
|
|
||||||
'allow-lan': boolean;
|
|
||||||
'bind-address': string;
|
|
||||||
mode: 'Rule' | 'Global' | 'Direct';
|
|
||||||
'mode-list': string[];
|
|
||||||
'log-level': 'debug' | 'info' | 'warn' | 'error';
|
|
||||||
ipv6: boolean;
|
|
||||||
tun: null | Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProxyHistoryEntry {
|
|
||||||
time: string;
|
|
||||||
delay: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProxyBase {
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
udp: boolean;
|
|
||||||
history: ProxyHistoryEntry[];
|
|
||||||
now?: string;
|
|
||||||
all?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Proxies {
|
|
||||||
proxies: Record<string, ProxyBase>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Delays = Record<string, number>;
|
|
||||||
}
|
|
||||||
16
fe-app-podkop/src/helpers/copyToClipboard.ts
Normal file
16
fe-app-podkop/src/helpers/copyToClipboard.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { showToast } from './showToast';
|
||||||
|
|
||||||
|
export function copyToClipboard(text: string) {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
showToast(_('Successfully copied!'), 'success');
|
||||||
|
} catch (_err) {
|
||||||
|
showToast(_('Failed to copy!'), 'error');
|
||||||
|
console.error('copyToClipboard - e', _err);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
15
fe-app-podkop/src/helpers/downloadAsTxt.ts
Normal file
15
fe-app-podkop/src/helpers/downloadAsTxt.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function downloadAsTxt(text: string, filename: string) {
|
||||||
|
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const safeName = filename.endsWith('.txt') ? filename : `${filename}.txt`;
|
||||||
|
link.download = safeName;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export function getBaseUrl(): string {
|
|
||||||
const { protocol, hostname } = window.location;
|
|
||||||
return `${protocol}//${hostname}`;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
export function getClashApiUrl(): string {
|
|
||||||
const { hostname } = window.location;
|
|
||||||
|
|
||||||
return `http://${hostname}:9090`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getClashWsUrl(): string {
|
export function getClashWsUrl(): string {
|
||||||
const { hostname } = window.location;
|
const { hostname } = window.location;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './getBaseUrl';
|
|
||||||
export * from './parseValueList';
|
export * from './parseValueList';
|
||||||
export * from './injectGlobalStyles';
|
export * from './injectGlobalStyles';
|
||||||
export * from './withTimeout';
|
export * from './withTimeout';
|
||||||
@@ -10,3 +9,5 @@ export * from './getClashApiUrl';
|
|||||||
export * from './splitProxyString';
|
export * from './splitProxyString';
|
||||||
export * from './preserveScrollForPage';
|
export * from './preserveScrollForPage';
|
||||||
export * from './parseQueryString';
|
export * from './parseQueryString';
|
||||||
|
export * from './svgEl';
|
||||||
|
export * from './insertIf';
|
||||||
|
|||||||
7
fe-app-podkop/src/helpers/insertIf.ts
Normal file
7
fe-app-podkop/src/helpers/insertIf.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export function insertIf<T>(condition: boolean, elements: Array<T>) {
|
||||||
|
return condition ? elements : ([] as Array<T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertIfObj<T>(condition: boolean, object: T) {
|
||||||
|
return condition ? object : ({} as T);
|
||||||
|
}
|
||||||
7
fe-app-podkop/src/helpers/normalizeCompiledVersion.ts
Normal file
7
fe-app-podkop/src/helpers/normalizeCompiledVersion.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export function normalizeCompiledVersion(version: string) {
|
||||||
|
if (version.includes('COMPILED')) {
|
||||||
|
return 'dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
24
fe-app-podkop/src/helpers/showToast.ts
Normal file
24
fe-app-podkop/src/helpers/showToast.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export function showToast(
|
||||||
|
message: string,
|
||||||
|
type: 'success' | 'error',
|
||||||
|
duration: number = 3000,
|
||||||
|
) {
|
||||||
|
let container = document.querySelector('.toast-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.className = 'toast-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast toast-${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
container.appendChild(toast);
|
||||||
|
setTimeout(() => toast.classList.add('visible'), 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('visible');
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
18
fe-app-podkop/src/helpers/svgEl.ts
Normal file
18
fe-app-podkop/src/helpers/svgEl.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export function svgEl<K extends keyof SVGElementTagNameMap>(
|
||||||
|
tag: K,
|
||||||
|
attrs: Partial<Record<string, string | number>> = {},
|
||||||
|
children: (SVGElement | null | undefined)[] = [],
|
||||||
|
): SVGElementTagNameMap[K] {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
const el = document.createElementNS(NS, tag);
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(attrs)) {
|
||||||
|
if (v != null) el.setAttribute(k, String(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
(Array.isArray(children) ? children : [children])
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach((ch) => el.appendChild(ch as SVGElement));
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { logger } from '../podkop';
|
||||||
|
|
||||||
export async function withTimeout<T>(
|
export async function withTimeout<T>(
|
||||||
promise: Promise<T>,
|
promise: Promise<T>,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
@@ -16,6 +18,6 @@ export async function withTimeout<T>(
|
|||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
const elapsed = performance.now() - start;
|
const elapsed = performance.now() - start;
|
||||||
console.log(`[${operationName}] Execution time: ${elapsed.toFixed(2)} ms`);
|
logger.info('[SHELL]', `[${operationName}] took ${elapsed.toFixed(2)} ms`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
fe-app-podkop/src/icons/index.ts
Normal file
18
fe-app-podkop/src/icons/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export * from './renderLoaderCircleIcon24';
|
||||||
|
export * from './renderCircleAlertIcon24';
|
||||||
|
export * from './renderCircleCheckIcon24';
|
||||||
|
export * from './renderCircleSlashIcon24';
|
||||||
|
export * from './renderCircleXIcon24';
|
||||||
|
export * from './renderCheckIcon24';
|
||||||
|
export * from './renderXIcon24';
|
||||||
|
export * from './renderTriangleAlertIcon24';
|
||||||
|
export * from './renderPauseIcon24';
|
||||||
|
export * from './renderPlayIcon24';
|
||||||
|
export * from './renderRotateCcwIcon24';
|
||||||
|
export * from './renderCircleStopIcon24';
|
||||||
|
export * from './renderCirclePlayIcon24';
|
||||||
|
export * from './renderCircleCheckBigIcon24';
|
||||||
|
export * from './renderSquareChartGanttIcon24';
|
||||||
|
export * from './renderCogIcon24';
|
||||||
|
export * from './renderSearchIcon24';
|
||||||
|
export * from './renderBookOpenTextIcon24';
|
||||||
28
fe-app-podkop/src/icons/renderBookOpenTextIcon24.ts
Normal file
28
fe-app-podkop/src/icons/renderBookOpenTextIcon24.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderBookOpenTextIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-book-open-text-icon lucide-book-open-text',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', { d: 'M12 7v14' }),
|
||||||
|
svgEl('path', { d: 'M16 12h2' }),
|
||||||
|
svgEl('path', { d: 'M16 8h2' }),
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z',
|
||||||
|
}),
|
||||||
|
svgEl('path', { d: 'M6 12h2' }),
|
||||||
|
svgEl('path', { d: 'M6 8h2' }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
23
fe-app-podkop/src/icons/renderCheckIcon24.ts
Normal file
23
fe-app-podkop/src/icons/renderCheckIcon24.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCheckIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-check-icon lucide-check',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M20 6 9 17l-5-5',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
39
fe-app-podkop/src/icons/renderCircleAlertIcon24.ts
Normal file
39
fe-app-podkop/src/icons/renderCircleAlertIcon24.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCircleAlertIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
width: '24',
|
||||||
|
height: '24',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-circle-alert-icon lucide-circle-alert',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('circle', {
|
||||||
|
cx: '12',
|
||||||
|
cy: '12',
|
||||||
|
r: '10',
|
||||||
|
}),
|
||||||
|
svgEl('line', {
|
||||||
|
x1: '12',
|
||||||
|
y1: '8',
|
||||||
|
x2: '12',
|
||||||
|
y2: '12',
|
||||||
|
}),
|
||||||
|
svgEl('line', {
|
||||||
|
x1: '12',
|
||||||
|
y1: '16',
|
||||||
|
x2: '12.01',
|
||||||
|
y2: '16',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
26
fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts
Normal file
26
fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCircleCheckBigIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-circle-check-big-icon lucide-circle-check-big',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M21.801 10A10 10 0 1 1 17 3.335',
|
||||||
|
}),
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'm9 11 3 3L22 4',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
30
fe-app-podkop/src/icons/renderCircleCheckIcon24.ts
Normal file
30
fe-app-podkop/src/icons/renderCircleCheckIcon24.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCircleCheckIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
width: '24',
|
||||||
|
height: '24',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-circle-check-icon lucide-circle-check',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('circle', {
|
||||||
|
cx: '12',
|
||||||
|
cy: '12',
|
||||||
|
r: '10',
|
||||||
|
}),
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M9 12l2 2 4-4',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
28
fe-app-podkop/src/icons/renderCirclePlayIcon24.ts
Normal file
28
fe-app-podkop/src/icons/renderCirclePlayIcon24.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCirclePlayIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-circle-play-icon lucide-circle-play',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z',
|
||||||
|
}),
|
||||||
|
svgEl('circle', {
|
||||||
|
cx: '12',
|
||||||
|
cy: '12',
|
||||||
|
r: '10',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
33
fe-app-podkop/src/icons/renderCircleSlashIcon24.ts
Normal file
33
fe-app-podkop/src/icons/renderCircleSlashIcon24.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCircleSlashIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
width: '24',
|
||||||
|
height: '24',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-circle-slash-icon lucide-circle-slash',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('circle', {
|
||||||
|
cx: '12',
|
||||||
|
cy: '12',
|
||||||
|
r: '10',
|
||||||
|
}),
|
||||||
|
svgEl('line', {
|
||||||
|
x1: '9',
|
||||||
|
y1: '15',
|
||||||
|
x2: '15',
|
||||||
|
y2: '9',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
32
fe-app-podkop/src/icons/renderCircleStopIcon24.ts
Normal file
32
fe-app-podkop/src/icons/renderCircleStopIcon24.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCircleStopIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-circle-stop-icon lucide-circle-stop',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('circle', {
|
||||||
|
cx: '12',
|
||||||
|
cy: '12',
|
||||||
|
r: '10',
|
||||||
|
}),
|
||||||
|
svgEl('rect', {
|
||||||
|
x: '9',
|
||||||
|
y: '9',
|
||||||
|
width: '6',
|
||||||
|
height: '6',
|
||||||
|
rx: '1',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
33
fe-app-podkop/src/icons/renderCircleXIcon24.ts
Normal file
33
fe-app-podkop/src/icons/renderCircleXIcon24.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCircleXIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
width: '24',
|
||||||
|
height: '24',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-circle-x-icon lucide-circle-x',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('circle', {
|
||||||
|
cx: '12',
|
||||||
|
cy: '12',
|
||||||
|
r: '10',
|
||||||
|
}),
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M15 9L9 15',
|
||||||
|
}),
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M9 9L15 15',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
34
fe-app-podkop/src/icons/renderCogIcon24.ts
Normal file
34
fe-app-podkop/src/icons/renderCogIcon24.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderCogIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-cog-icon lucide-cog',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', { d: 'M11 10.27 7 3.34' }),
|
||||||
|
svgEl('path', { d: 'm11 13.73-4 6.93' }),
|
||||||
|
svgEl('path', { d: 'M12 22v-2' }),
|
||||||
|
svgEl('path', { d: 'M12 2v2' }),
|
||||||
|
svgEl('path', { d: 'M14 12h8' }),
|
||||||
|
svgEl('path', { d: 'm17 20.66-1-1.73' }),
|
||||||
|
svgEl('path', { d: 'm17 3.34-1 1.73' }),
|
||||||
|
svgEl('path', { d: 'M2 12h2' }),
|
||||||
|
svgEl('path', { d: 'm20.66 17-1.73-1' }),
|
||||||
|
svgEl('path', { d: 'm20.66 7-1.73 1' }),
|
||||||
|
svgEl('path', { d: 'm3.34 17 1.73-1' }),
|
||||||
|
svgEl('path', { d: 'm3.34 7 1.73 1' }),
|
||||||
|
svgEl('circle', { cx: '12', cy: '12', r: '2' }),
|
||||||
|
svgEl('circle', { cx: '12', cy: '12', r: '8' }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
32
fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts
Normal file
32
fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderLoaderCircleIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-loader-circle rotate',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M21 12a9 9 0 1 1-6.219-8.56',
|
||||||
|
}),
|
||||||
|
svgEl('animateTransform', {
|
||||||
|
attributeName: 'transform',
|
||||||
|
attributeType: 'XML',
|
||||||
|
type: 'rotate',
|
||||||
|
from: '0 12 12',
|
||||||
|
to: '360 12 12',
|
||||||
|
dur: '1s',
|
||||||
|
repeatCount: 'indefinite',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
34
fe-app-podkop/src/icons/renderPauseIcon24.ts
Normal file
34
fe-app-podkop/src/icons/renderPauseIcon24.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderPauseIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-pause-icon lucide-pause',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('rect', {
|
||||||
|
x: '14',
|
||||||
|
y: '3',
|
||||||
|
width: '5',
|
||||||
|
height: '18',
|
||||||
|
rx: '1',
|
||||||
|
}),
|
||||||
|
svgEl('rect', {
|
||||||
|
x: '5',
|
||||||
|
y: '3',
|
||||||
|
width: '5',
|
||||||
|
height: '18',
|
||||||
|
rx: '1',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
23
fe-app-podkop/src/icons/renderPlayIcon24.ts
Normal file
23
fe-app-podkop/src/icons/renderPlayIcon24.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderPlayIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-play-icon lucide-play',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
26
fe-app-podkop/src/icons/renderRotateCcwIcon24.ts
Normal file
26
fe-app-podkop/src/icons/renderRotateCcwIcon24.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderRotateCcwIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-rotate-ccw-icon lucide-rotate-ccw',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8',
|
||||||
|
}),
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'M3 3v5h5',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
22
fe-app-podkop/src/icons/renderSearchIcon24.ts
Normal file
22
fe-app-podkop/src/icons/renderSearchIcon24.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderSearchIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-search-icon lucide-search',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', { d: 'm21 21-4.34-4.34' }),
|
||||||
|
svgEl('circle', { cx: '11', cy: '11', r: '8' }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
30
fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts
Normal file
30
fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderSquareChartGanttIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('rect', {
|
||||||
|
width: '18',
|
||||||
|
height: '18',
|
||||||
|
x: '3',
|
||||||
|
y: '3',
|
||||||
|
rx: '2',
|
||||||
|
}),
|
||||||
|
svgEl('path', { d: 'M9 8h7' }),
|
||||||
|
svgEl('path', { d: 'M8 12h6' }),
|
||||||
|
svgEl('path', { d: 'M11 16h5' }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
25
fe-app-podkop/src/icons/renderTriangleAlertIcon24.ts
Normal file
25
fe-app-podkop/src/icons/renderTriangleAlertIcon24.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderTriangleAlertIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-triangle-alert-icon lucide-triangle-alert',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
svgEl('path', {
|
||||||
|
d: 'm21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3',
|
||||||
|
}),
|
||||||
|
svgEl('path', { d: 'M12 9v4' }),
|
||||||
|
svgEl('path', { d: 'M12 17h.01' }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
19
fe-app-podkop/src/icons/renderXIcon24.ts
Normal file
19
fe-app-podkop/src/icons/renderXIcon24.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { svgEl } from '../helpers';
|
||||||
|
|
||||||
|
export function renderXIcon24() {
|
||||||
|
const NS = 'http://www.w3.org/2000/svg';
|
||||||
|
return svgEl(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: NS,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
class: 'lucide lucide-x-icon lucide-x',
|
||||||
|
},
|
||||||
|
[svgEl('path', { d: 'M18 6 6 18' }), svgEl('path', { d: 'm6 6 12 12' })],
|
||||||
|
);
|
||||||
|
}
|
||||||
10
fe-app-podkop/src/luci.d.ts
vendored
10
fe-app-podkop/src/luci.d.ts
vendored
@@ -35,6 +35,16 @@ declare global {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const _ = (_key: string) => string;
|
const _ = (_key: string) => string;
|
||||||
|
|
||||||
|
const ui = {
|
||||||
|
showModal: (_title: stirng, _content: HtmlElement) => undefined,
|
||||||
|
hideModal: () => undefined,
|
||||||
|
addNotification: (
|
||||||
|
_title: string,
|
||||||
|
_children: HtmlElement | HtmlElement[],
|
||||||
|
_className?: string,
|
||||||
|
) => undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
'require baseclass';
|
'require baseclass';
|
||||||
'require fs';
|
'require fs';
|
||||||
'require uci';
|
'require uci';
|
||||||
|
'require ui';
|
||||||
|
|
||||||
|
if (typeof structuredClone !== 'function')
|
||||||
|
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
|
||||||
|
|
||||||
export * from './validators';
|
export * from './validators';
|
||||||
export * from './helpers';
|
export * from './helpers';
|
||||||
export * from './clash';
|
|
||||||
export * from './podkop';
|
export * from './podkop';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
|
|||||||
69
fe-app-podkop/src/partials/button/renderButton.ts
Normal file
69
fe-app-podkop/src/partials/button/renderButton.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { insertIf } from '../../helpers';
|
||||||
|
import { renderLoaderCircleIcon24 } from '../../icons';
|
||||||
|
|
||||||
|
interface IRenderButtonProps {
|
||||||
|
classNames?: string[];
|
||||||
|
disabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
icon?: () => SVGSVGElement;
|
||||||
|
onClick: () => void;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderButton({
|
||||||
|
classNames = [],
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
onClick,
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
}: IRenderButtonProps) {
|
||||||
|
const hasIcon = !!loading || !!icon;
|
||||||
|
|
||||||
|
function getWrappedIcon() {
|
||||||
|
const iconWrap = E('span', {
|
||||||
|
class: 'pdk-partial-button__icon',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
iconWrap.appendChild(renderLoaderCircleIcon24());
|
||||||
|
|
||||||
|
return iconWrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
iconWrap.appendChild(icon());
|
||||||
|
|
||||||
|
return iconWrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconWrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClass() {
|
||||||
|
return [
|
||||||
|
'btn',
|
||||||
|
'pdk-partial-button',
|
||||||
|
...insertIf(Boolean(disabled), ['pdk-partial-button--disabled']),
|
||||||
|
...insertIf(Boolean(loading), ['pdk-partial-button--loading']),
|
||||||
|
...insertIf(Boolean(hasIcon), ['pdk-partial-button--with-icon']),
|
||||||
|
...classNames,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisabled() {
|
||||||
|
if (loading || disabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return E(
|
||||||
|
'button',
|
||||||
|
{ class: getClass(), disabled: getDisabled(), click: onClick },
|
||||||
|
[...insertIf(hasIcon, [getWrappedIcon()]), E('span', {}, text)],
|
||||||
|
);
|
||||||
|
}
|
||||||
33
fe-app-podkop/src/partials/button/styles.ts
Normal file
33
fe-app-podkop/src/partials/button/styles.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// language=CSS
|
||||||
|
export const styles = `
|
||||||
|
.pdk-partial-button {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk-partial-button--with-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk-partial-button--loading {
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk-partial-button--disabled {
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk-partial-button__icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk-partial-button__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk-partial-button__icon svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
10
fe-app-podkop/src/partials/index.ts
Normal file
10
fe-app-podkop/src/partials/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { styles as ButtonStyles } from './button/styles';
|
||||||
|
import { styles as ModalStyles } from './modal/styles';
|
||||||
|
|
||||||
|
export * from './button/renderButton';
|
||||||
|
export * from './modal/renderModal';
|
||||||
|
|
||||||
|
export const PartialStyles = `
|
||||||
|
${ButtonStyles}
|
||||||
|
${ModalStyles}
|
||||||
|
`;
|
||||||
32
fe-app-podkop/src/partials/modal/renderModal.ts
Normal file
32
fe-app-podkop/src/partials/modal/renderModal.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { renderButton } from '../button/renderButton';
|
||||||
|
import { copyToClipboard } from '../../helpers/copyToClipboard';
|
||||||
|
import { downloadAsTxt } from '../../helpers/downloadAsTxt';
|
||||||
|
|
||||||
|
export function renderModal(text: string, name: string) {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{ class: 'pdk-partial-modal__body' },
|
||||||
|
E('div', {}, [
|
||||||
|
E('pre', { class: 'pdk-partial-modal__content' }, E('code', {}, text)),
|
||||||
|
|
||||||
|
E('div', { class: 'pdk-partial-modal__footer' }, [
|
||||||
|
renderButton({
|
||||||
|
classNames: ['cbi-button-apply'],
|
||||||
|
text: _('Download'),
|
||||||
|
onClick: () => downloadAsTxt(text, name),
|
||||||
|
}),
|
||||||
|
renderButton({
|
||||||
|
classNames: ['cbi-button-apply'],
|
||||||
|
text: _('Copy'),
|
||||||
|
onClick: () =>
|
||||||
|
copyToClipboard(` \`\`\`${name} \n ${text} \n \`\`\``),
|
||||||
|
}),
|
||||||
|
renderButton({
|
||||||
|
classNames: ['cbi-button-remove'],
|
||||||
|
text: _('Close'),
|
||||||
|
onClick: ui.hideModal,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
20
fe-app-podkop/src/partials/modal/styles.ts
Normal file
20
fe-app-podkop/src/partials/modal/styles.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// language=CSS
|
||||||
|
export const styles = `
|
||||||
|
|
||||||
|
.pdk-partial-modal__body {}
|
||||||
|
|
||||||
|
.pdk-partial-modal__content {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: scroll;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk-partial-modal__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk-partial-modal__footer button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
53
fe-app-podkop/src/podkop/api.ts
Normal file
53
fe-app-podkop/src/podkop/api.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { withTimeout } from '../helpers';
|
||||||
|
|
||||||
|
export async function createBaseApiRequest<T>(
|
||||||
|
fetchFn: () => Promise<Response>,
|
||||||
|
options?: {
|
||||||
|
timeoutMs?: number;
|
||||||
|
operationName?: string;
|
||||||
|
timeoutMessage?: string;
|
||||||
|
},
|
||||||
|
): Promise<IBaseApiResponse<T>> {
|
||||||
|
const wrappedFn = () =>
|
||||||
|
options?.timeoutMs && options?.operationName
|
||||||
|
? withTimeout(
|
||||||
|
fetchFn(),
|
||||||
|
options.timeoutMs,
|
||||||
|
options.operationName,
|
||||||
|
options.timeoutMessage,
|
||||||
|
)
|
||||||
|
: fetchFn();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await wrappedFn();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
success: false as const,
|
||||||
|
message: `${_('HTTP error')} ${response.status}: ${response.statusText}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: T = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true as const,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false as const,
|
||||||
|
message: e instanceof Error ? e.message : _('Unknown error'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IBaseApiResponse<T> =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
29
fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts
Normal file
29
fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { PodkopShellMethods } from '../methods';
|
||||||
|
import { store } from '../services';
|
||||||
|
|
||||||
|
export async function fetchServicesInfo() {
|
||||||
|
const [podkop, singbox] = await Promise.all([
|
||||||
|
PodkopShellMethods.getStatus(),
|
||||||
|
PodkopShellMethods.getSingBoxStatus(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!podkop.success || !singbox.success) {
|
||||||
|
store.set({
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: { singbox: 0, podkop: 0 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (podkop.success && singbox.success) {
|
||||||
|
store.set({
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
data: { singbox: singbox.data.running, podkop: podkop.data.enabled },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1
fe-app-podkop/src/podkop/fetchers/index.ts
Normal file
1
fe-app-podkop/src/podkop/fetchers/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './fetchServicesInfo';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { getConfigSections } from './getConfigSections';
|
||||||
|
|
||||||
|
export async function getClashApiSecret() {
|
||||||
|
const sections = await getConfigSections();
|
||||||
|
|
||||||
|
const settings = sections.find((section) => section['.type'] === 'settings');
|
||||||
|
|
||||||
|
return settings?.yacd_secret_key || '';
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Podkop } from '../types';
|
import { Podkop } from '../../types';
|
||||||
|
|
||||||
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
|
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
|
||||||
return uci.load('podkop').then(() => uci.sections('podkop'));
|
return uci.load('podkop').then(() => uci.sections('podkop'));
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Podkop } from '../types';
|
|
||||||
import { getConfigSections } from './getConfigSections';
|
import { getConfigSections } from './getConfigSections';
|
||||||
import { getClashProxies } from '../../clash';
|
import { Podkop } from '../../types';
|
||||||
import { getProxyUrlName, splitProxyString } from '../../helpers';
|
import { getProxyUrlName, splitProxyString } from '../../../helpers';
|
||||||
|
import { PodkopShellMethods } from '../shell';
|
||||||
|
|
||||||
interface IGetDashboardSectionsResponse {
|
interface IGetDashboardSectionsResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -10,7 +10,7 @@ interface IGetDashboardSectionsResponse {
|
|||||||
|
|
||||||
export async function getDashboardSections(): Promise<IGetDashboardSectionsResponse> {
|
export async function getDashboardSections(): Promise<IGetDashboardSectionsResponse> {
|
||||||
const configSections = await getConfigSections();
|
const configSections = await getConfigSections();
|
||||||
const clashProxies = await getClashProxies();
|
const clashProxies = await PodkopShellMethods.getClashApiProxies();
|
||||||
|
|
||||||
if (!clashProxies.success) {
|
if (!clashProxies.success) {
|
||||||
return {
|
return {
|
||||||
@@ -27,9 +27,12 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
|
|||||||
);
|
);
|
||||||
|
|
||||||
const data = configSections
|
const data = configSections
|
||||||
.filter((section) => section.mode !== 'block')
|
.filter(
|
||||||
|
(section) =>
|
||||||
|
section.connection_type !== 'block' && section['.type'] !== 'settings',
|
||||||
|
)
|
||||||
.map((section) => {
|
.map((section) => {
|
||||||
if (section.mode === 'proxy') {
|
if (section.connection_type === 'proxy') {
|
||||||
if (section.proxy_config_type === 'url') {
|
if (section.proxy_config_type === 'url') {
|
||||||
const outbound = proxies.find(
|
const outbound = proxies.find(
|
||||||
(proxy) => proxy.code === `${section['.name']}-out`,
|
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||||
@@ -122,7 +125,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section.mode === 'vpn') {
|
if (section.connection_type === 'vpn') {
|
||||||
const outbound = proxies.find(
|
const outbound = proxies.find(
|
||||||
(proxy) => proxy.code === `${section['.name']}-out`,
|
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||||
);
|
);
|
||||||
9
fe-app-podkop/src/podkop/methods/custom/index.ts
Normal file
9
fe-app-podkop/src/podkop/methods/custom/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { getConfigSections } from './getConfigSections';
|
||||||
|
import { getDashboardSections } from './getDashboardSections';
|
||||||
|
import { getClashApiSecret } from './getClashApiSecret';
|
||||||
|
|
||||||
|
export const CustomPodkopMethods = {
|
||||||
|
getConfigSections,
|
||||||
|
getDashboardSections,
|
||||||
|
getClashApiSecret,
|
||||||
|
};
|
||||||
23
fe-app-podkop/src/podkop/methods/fakeip/getFakeIpCheck.ts
Normal file
23
fe-app-podkop/src/podkop/methods/fakeip/getFakeIpCheck.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { FAKEIP_CHECK_DOMAIN } from '../../../constants';
|
||||||
|
import { createBaseApiRequest, IBaseApiResponse } from '../../api';
|
||||||
|
|
||||||
|
interface IGetFakeIpCheckResponse {
|
||||||
|
fakeip: boolean;
|
||||||
|
IP: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFakeIpCheck(): Promise<
|
||||||
|
IBaseApiResponse<IGetFakeIpCheckResponse>
|
||||||
|
> {
|
||||||
|
return createBaseApiRequest<IGetFakeIpCheckResponse>(
|
||||||
|
() =>
|
||||||
|
fetch(`https://${FAKEIP_CHECK_DOMAIN}/check`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
operationName: 'getFakeIpCheck',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
23
fe-app-podkop/src/podkop/methods/fakeip/getIpCheck.ts
Normal file
23
fe-app-podkop/src/podkop/methods/fakeip/getIpCheck.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { IP_CHECK_DOMAIN } from '../../../constants';
|
||||||
|
import { createBaseApiRequest, IBaseApiResponse } from '../../api';
|
||||||
|
|
||||||
|
interface IGetIpCheckResponse {
|
||||||
|
fakeip: boolean;
|
||||||
|
IP: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getIpCheck(): Promise<
|
||||||
|
IBaseApiResponse<IGetIpCheckResponse>
|
||||||
|
> {
|
||||||
|
return createBaseApiRequest<IGetIpCheckResponse>(
|
||||||
|
() =>
|
||||||
|
fetch(`https://${IP_CHECK_DOMAIN}/check`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
operationName: 'getIpCheck',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
7
fe-app-podkop/src/podkop/methods/fakeip/index.ts
Normal file
7
fe-app-podkop/src/podkop/methods/fakeip/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { getFakeIpCheck } from './getFakeIpCheck';
|
||||||
|
import { getIpCheck } from './getIpCheck';
|
||||||
|
|
||||||
|
export const RemoteFakeIPMethods = {
|
||||||
|
getFakeIpCheck,
|
||||||
|
getIpCheck,
|
||||||
|
};
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { executeShellCommand } from '../../helpers';
|
|
||||||
|
|
||||||
export async function getPodkopStatus(): Promise<{
|
|
||||||
enabled: number;
|
|
||||||
status: string;
|
|
||||||
}> {
|
|
||||||
const response = await executeShellCommand({
|
|
||||||
command: '/usr/bin/podkop',
|
|
||||||
args: ['get_status'],
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.stdout) {
|
|
||||||
return JSON.parse(response.stdout.replace(/\n/g, '')) as {
|
|
||||||
enabled: number;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { enabled: 0, status: 'unknown' };
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { executeShellCommand } from '../../helpers';
|
|
||||||
|
|
||||||
export async function getSingboxStatus(): Promise<{
|
|
||||||
running: number;
|
|
||||||
enabled: number;
|
|
||||||
status: string;
|
|
||||||
}> {
|
|
||||||
const response = await executeShellCommand({
|
|
||||||
command: '/usr/bin/podkop',
|
|
||||||
args: ['get_sing_box_status'],
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.stdout) {
|
|
||||||
return JSON.parse(response.stdout.replace(/\n/g, '')) as {
|
|
||||||
running: number;
|
|
||||||
enabled: number;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { running: 0, enabled: 0, status: 'unknown' };
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './getConfigSections';
|
export * from './custom';
|
||||||
export * from './getDashboardSections';
|
export * from './fakeip';
|
||||||
export * from './getPodkopStatus';
|
export * from './shell';
|
||||||
export * from './getSingboxStatus';
|
|
||||||
|
|||||||
33
fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts
Normal file
33
fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { executeShellCommand } from '../../../helpers';
|
||||||
|
import { Podkop } from '../../types';
|
||||||
|
|
||||||
|
export async function callBaseMethod<T>(
|
||||||
|
method: Podkop.AvailableMethods,
|
||||||
|
args: string[] = [],
|
||||||
|
command: string = '/usr/bin/podkop',
|
||||||
|
): Promise<Podkop.MethodResponse<T>> {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command,
|
||||||
|
args: [method as string, ...args],
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.stdout) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: JSON.parse(response.stdout) as T,
|
||||||
|
};
|
||||||
|
} catch (_e) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: response.stdout as T,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
87
fe-app-podkop/src/podkop/methods/shell/index.ts
Normal file
87
fe-app-podkop/src/podkop/methods/shell/index.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { callBaseMethod } from './callBaseMethod';
|
||||||
|
import { ClashAPI, Podkop } from '../../types';
|
||||||
|
|
||||||
|
export const PodkopShellMethods = {
|
||||||
|
checkDNSAvailable: async () =>
|
||||||
|
callBaseMethod<Podkop.DnsCheckResult>(
|
||||||
|
Podkop.AvailableMethods.CHECK_DNS_AVAILABLE,
|
||||||
|
),
|
||||||
|
checkFakeIP: async () =>
|
||||||
|
callBaseMethod<Podkop.FakeIPCheckResult>(
|
||||||
|
Podkop.AvailableMethods.CHECK_FAKEIP,
|
||||||
|
),
|
||||||
|
checkNftRules: async () =>
|
||||||
|
callBaseMethod<Podkop.NftRulesCheckResult>(
|
||||||
|
Podkop.AvailableMethods.CHECK_NFT_RULES,
|
||||||
|
),
|
||||||
|
getStatus: async () =>
|
||||||
|
callBaseMethod<Podkop.GetStatus>(Podkop.AvailableMethods.GET_STATUS),
|
||||||
|
checkSingBox: async () =>
|
||||||
|
callBaseMethod<Podkop.SingBoxCheckResult>(
|
||||||
|
Podkop.AvailableMethods.CHECK_SING_BOX,
|
||||||
|
),
|
||||||
|
getSingBoxStatus: async () =>
|
||||||
|
callBaseMethod<Podkop.GetSingBoxStatus>(
|
||||||
|
Podkop.AvailableMethods.GET_SING_BOX_STATUS,
|
||||||
|
),
|
||||||
|
getClashApiProxies: async () =>
|
||||||
|
callBaseMethod<ClashAPI.Proxies>(Podkop.AvailableMethods.CLASH_API, [
|
||||||
|
Podkop.AvailableClashAPIMethods.GET_PROXIES,
|
||||||
|
]),
|
||||||
|
getClashApiProxyLatency: async (tag: string) =>
|
||||||
|
callBaseMethod<Podkop.GetClashApiProxyLatency>(
|
||||||
|
Podkop.AvailableMethods.CLASH_API,
|
||||||
|
[Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, '5000'],
|
||||||
|
),
|
||||||
|
getClashApiGroupLatency: async (tag: string) =>
|
||||||
|
callBaseMethod<Podkop.GetClashApiGroupLatency>(
|
||||||
|
Podkop.AvailableMethods.CLASH_API,
|
||||||
|
[Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, '10000'],
|
||||||
|
),
|
||||||
|
setClashApiGroupProxy: async (group: string, proxy: string) =>
|
||||||
|
callBaseMethod<unknown>(Podkop.AvailableMethods.CLASH_API, [
|
||||||
|
Podkop.AvailableClashAPIMethods.SET_GROUP_PROXY,
|
||||||
|
group,
|
||||||
|
proxy,
|
||||||
|
]),
|
||||||
|
restart: async () =>
|
||||||
|
callBaseMethod<unknown>(
|
||||||
|
Podkop.AvailableMethods.RESTART,
|
||||||
|
[],
|
||||||
|
'/etc/init.d/podkop',
|
||||||
|
),
|
||||||
|
start: async () =>
|
||||||
|
callBaseMethod<unknown>(
|
||||||
|
Podkop.AvailableMethods.START,
|
||||||
|
[],
|
||||||
|
'/etc/init.d/podkop',
|
||||||
|
),
|
||||||
|
stop: async () =>
|
||||||
|
callBaseMethod<unknown>(
|
||||||
|
Podkop.AvailableMethods.STOP,
|
||||||
|
[],
|
||||||
|
'/etc/init.d/podkop',
|
||||||
|
),
|
||||||
|
enable: async () =>
|
||||||
|
callBaseMethod<unknown>(
|
||||||
|
Podkop.AvailableMethods.ENABLE,
|
||||||
|
[],
|
||||||
|
'/etc/init.d/podkop',
|
||||||
|
),
|
||||||
|
disable: async () =>
|
||||||
|
callBaseMethod<unknown>(
|
||||||
|
Podkop.AvailableMethods.DISABLE,
|
||||||
|
[],
|
||||||
|
'/etc/init.d/podkop',
|
||||||
|
),
|
||||||
|
globalCheck: async () =>
|
||||||
|
callBaseMethod<unknown>(Podkop.AvailableMethods.GLOBAL_CHECK),
|
||||||
|
showSingBoxConfig: async () =>
|
||||||
|
callBaseMethod<unknown>(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG),
|
||||||
|
checkLogs: async () =>
|
||||||
|
callBaseMethod<unknown>(Podkop.AvailableMethods.CHECK_LOGS),
|
||||||
|
getSystemInfo: async () =>
|
||||||
|
callBaseMethod<Podkop.GetSystemInfo>(
|
||||||
|
Podkop.AvailableMethods.GET_SYSTEM_INFO,
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import { TabServiceInstance } from './tab.service';
|
import { TabServiceInstance } from './tab.service';
|
||||||
import { store } from '../../store';
|
import { store } from './store.service';
|
||||||
|
import { logger } from './logger.service';
|
||||||
|
import { PodkopLogWatcher } from './podkopLogWatcher.service';
|
||||||
|
import { PodkopShellMethods } from '../methods';
|
||||||
|
|
||||||
export function coreService() {
|
export function coreService() {
|
||||||
TabServiceInstance.onChange((activeId, tabs) => {
|
TabServiceInstance.onChange((activeId, tabs) => {
|
||||||
|
logger.info('[TAB]', activeId);
|
||||||
store.set({
|
store.set({
|
||||||
tabService: {
|
tabService: {
|
||||||
current: activeId || '',
|
current: activeId || '',
|
||||||
@@ -10,4 +14,31 @@ export function coreService() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const watcher = PodkopLogWatcher.getInstance();
|
||||||
|
|
||||||
|
watcher.init(
|
||||||
|
async () => {
|
||||||
|
const logs = await PodkopShellMethods.checkLogs();
|
||||||
|
|
||||||
|
if (logs.success) {
|
||||||
|
return logs.data as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
{
|
||||||
|
intervalMs: 3000,
|
||||||
|
onNewLog: (line) => {
|
||||||
|
if (
|
||||||
|
line.toLowerCase().includes('[error]') ||
|
||||||
|
line.toLowerCase().includes('[fatal]')
|
||||||
|
) {
|
||||||
|
ui.addNotification('Podkop Error', E('div', {}, line), 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watcher.start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
export * from './tab.service';
|
export * from './tab.service';
|
||||||
export * from './core.service';
|
export * from './core.service';
|
||||||
|
export * from './socket.service';
|
||||||
|
export * from './store.service';
|
||||||
|
export * from './logger.service';
|
||||||
|
|||||||
66
fe-app-podkop/src/podkop/services/logger.service.ts
Normal file
66
fe-app-podkop/src/podkop/services/logger.service.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { downloadAsTxt } from '../../helpers/downloadAsTxt';
|
||||||
|
|
||||||
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
|
export class Logger {
|
||||||
|
private logs: string[] = [];
|
||||||
|
private readonly levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
|
||||||
|
|
||||||
|
private format(level: LogLevel, ...args: unknown[]): string {
|
||||||
|
return `[${level.toUpperCase()}] ${args.join(' ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private push(level: LogLevel, ...args: unknown[]): void {
|
||||||
|
if (!this.levels.includes(level)) level = 'info';
|
||||||
|
const message = this.format(level, ...args);
|
||||||
|
this.logs.push(message);
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'error':
|
||||||
|
console.error(message);
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
console.warn(message);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
console.info(message);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(...args: unknown[]): void {
|
||||||
|
this.push('debug', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(...args: unknown[]): void {
|
||||||
|
this.push('info', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(...args: unknown[]): void {
|
||||||
|
this.push('warn', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(...args: unknown[]): void {
|
||||||
|
this.push('error', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.logs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogs(): string {
|
||||||
|
return this.logs.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
download(filename = 'logs.txt'): void {
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
console.warn('Logger.download() доступен только в браузере');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadAsTxt(this.getLogs(), filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Logger();
|
||||||
116
fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts
Normal file
116
fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { logger } from './logger.service';
|
||||||
|
|
||||||
|
export type LogFetcher = () => Promise<string> | string;
|
||||||
|
|
||||||
|
export interface PodkopLogWatcherOptions {
|
||||||
|
intervalMs?: number;
|
||||||
|
onNewLog?: (line: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PodkopLogWatcher {
|
||||||
|
private static instance: PodkopLogWatcher;
|
||||||
|
private fetcher?: LogFetcher;
|
||||||
|
private onNewLog?: (line: string) => void;
|
||||||
|
private intervalMs = 5000;
|
||||||
|
private lastLines = new Set<string>();
|
||||||
|
private timer?: ReturnType<typeof setInterval>;
|
||||||
|
private running = false;
|
||||||
|
private paused = false;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.hidden) this.pause();
|
||||||
|
else this.resume();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): PodkopLogWatcher {
|
||||||
|
if (!PodkopLogWatcher.instance) {
|
||||||
|
PodkopLogWatcher.instance = new PodkopLogWatcher();
|
||||||
|
}
|
||||||
|
return PodkopLogWatcher.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(fetcher: LogFetcher, options?: PodkopLogWatcherOptions): void {
|
||||||
|
this.fetcher = fetcher;
|
||||||
|
this.onNewLog = options?.onNewLog;
|
||||||
|
this.intervalMs = options?.intervalMs ?? 5000;
|
||||||
|
logger.info(
|
||||||
|
'[PodkopLogWatcher]',
|
||||||
|
`initialized (interval: ${this.intervalMs}ms)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkOnce(): Promise<void> {
|
||||||
|
if (!this.fetcher) {
|
||||||
|
logger.warn('[PodkopLogWatcher]', 'fetcher not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.paused) {
|
||||||
|
logger.debug('[PodkopLogWatcher]', 'skipped check — tab not visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = await this.fetcher();
|
||||||
|
const lines = raw.split('\n').filter(Boolean);
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!this.lastLines.has(line)) {
|
||||||
|
this.lastLines.add(line);
|
||||||
|
this.onNewLog?.(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lastLines.size > 500) {
|
||||||
|
const arr = Array.from(this.lastLines);
|
||||||
|
this.lastLines = new Set(arr.slice(-500));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[PodkopLogWatcher]', 'failed to read logs:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
if (this.running) return;
|
||||||
|
if (!this.fetcher) {
|
||||||
|
logger.warn('[PodkopLogWatcher]', 'attempted to start without fetcher');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.running = true;
|
||||||
|
this.timer = setInterval(() => this.checkOnce(), this.intervalMs);
|
||||||
|
logger.info(
|
||||||
|
'[PodkopLogWatcher]',
|
||||||
|
`started (interval: ${this.intervalMs}ms)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
if (!this.running) return;
|
||||||
|
this.running = false;
|
||||||
|
if (this.timer) clearInterval(this.timer);
|
||||||
|
logger.info('[PodkopLogWatcher]', 'stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
pause(): void {
|
||||||
|
if (!this.running || this.paused) return;
|
||||||
|
this.paused = true;
|
||||||
|
logger.info('[PodkopLogWatcher]', 'paused (tab not visible)');
|
||||||
|
}
|
||||||
|
|
||||||
|
resume(): void {
|
||||||
|
if (!this.running || !this.paused) return;
|
||||||
|
this.paused = false;
|
||||||
|
logger.info('[PodkopLogWatcher]', 'resumed (tab active)');
|
||||||
|
this.checkOnce(); // сразу проверить, не появились ли новые логи
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.lastLines.clear();
|
||||||
|
logger.info('[PodkopLogWatcher]', 'log history reset');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { logger } from './logger.service';
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
type Listener = (data: any) => void;
|
type Listener = (data: any) => void;
|
||||||
type ErrorListener = (error: Event | string) => void;
|
type ErrorListener = (error: Event | string) => void;
|
||||||
@@ -18,10 +20,48 @@ class SocketManager {
|
|||||||
return SocketManager.instance;
|
return SocketManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetAll(): void {
|
||||||
|
for (const [url, ws] of this.sockets.entries()) {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
ws.readyState === WebSocket.OPEN ||
|
||||||
|
ws.readyState === WebSocket.CONNECTING
|
||||||
|
) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
'[SOCKET]',
|
||||||
|
`resetAll: failed to close socket ${url}`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sockets.clear();
|
||||||
|
this.listeners.clear();
|
||||||
|
this.errorListeners.clear();
|
||||||
|
this.connected.clear();
|
||||||
|
logger.info('[SOCKET]', 'All connections and state have been reset.');
|
||||||
|
}
|
||||||
|
|
||||||
connect(url: string): void {
|
connect(url: string): void {
|
||||||
if (this.sockets.has(url)) return;
|
if (this.sockets.has(url)) return;
|
||||||
|
|
||||||
const ws = new WebSocket(url);
|
let ws: WebSocket;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
'[SOCKET]',
|
||||||
|
`failed to construct WebSocket for ${url}:`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
this.triggerError(url, err instanceof Event ? err : String(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.sockets.set(url, ws);
|
this.sockets.set(url, ws);
|
||||||
this.connected.set(url, false);
|
this.connected.set(url, false);
|
||||||
this.listeners.set(url, new Set());
|
this.listeners.set(url, new Set());
|
||||||
@@ -29,7 +69,7 @@ class SocketManager {
|
|||||||
|
|
||||||
ws.addEventListener('open', () => {
|
ws.addEventListener('open', () => {
|
||||||
this.connected.set(url, true);
|
this.connected.set(url, true);
|
||||||
console.info(`Connected: ${url}`);
|
logger.info('[SOCKET]', 'Connected to', url);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener('message', (event) => {
|
ws.addEventListener('message', (event) => {
|
||||||
@@ -39,7 +79,7 @@ class SocketManager {
|
|||||||
try {
|
try {
|
||||||
handler(event.data);
|
handler(event.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Handler error for ${url}:`, err);
|
logger.error('[SOCKET]', `Handler error for ${url}:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,26 +87,32 @@ class SocketManager {
|
|||||||
|
|
||||||
ws.addEventListener('close', () => {
|
ws.addEventListener('close', () => {
|
||||||
this.connected.set(url, false);
|
this.connected.set(url, false);
|
||||||
console.warn(`Disconnected: ${url}`);
|
logger.warn('[SOCKET]', `Disconnected: ${url}`);
|
||||||
this.triggerError(url, 'Connection closed');
|
this.triggerError(url, 'Connection closed');
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener('error', (err) => {
|
ws.addEventListener('error', (err) => {
|
||||||
console.error(`Socket error for ${url}:`, err);
|
logger.error('[SOCKET]', `Socket error for ${url}:`, err);
|
||||||
this.triggerError(url, err);
|
this.triggerError(url, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
subscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||||
|
if (!this.errorListeners.has(url)) {
|
||||||
|
this.errorListeners.set(url, new Set());
|
||||||
|
}
|
||||||
|
if (onError) {
|
||||||
|
this.errorListeners.get(url)?.add(onError);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.sockets.has(url)) {
|
if (!this.sockets.has(url)) {
|
||||||
this.connect(url);
|
this.connect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listeners.get(url)?.add(listener);
|
if (!this.listeners.has(url)) {
|
||||||
|
this.listeners.set(url, new Set());
|
||||||
if (onError) {
|
|
||||||
this.errorListeners.get(url)?.add(onError);
|
|
||||||
}
|
}
|
||||||
|
this.listeners.get(url)?.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||||
@@ -82,7 +128,7 @@ class SocketManager {
|
|||||||
if (ws && this.connected.get(url)) {
|
if (ws && this.connected.get(url)) {
|
||||||
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Cannot send: not connected to ${url}`);
|
logger.warn('[SOCKET]', `Cannot send: not connected to ${url}`);
|
||||||
this.triggerError(url, 'Not connected');
|
this.triggerError(url, 'Not connected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +157,7 @@ class SocketManager {
|
|||||||
try {
|
try {
|
||||||
cb(err);
|
cb(err);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error handler threw for ${url}:`, e);
|
logger.error('[SOCKET]', `Error handler threw for ${url}:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Podkop } from './podkop/types';
|
import { Podkop } from '../types';
|
||||||
|
import { initialDiagnosticStore } from '../tabs/diagnostic/diagnostic.store';
|
||||||
|
|
||||||
function jsonStableStringify<T, V>(obj: T): string {
|
function jsonStableStringify<T, V>(obj: T): string {
|
||||||
return JSON.stringify(obj, (_, value) => {
|
return JSON.stringify(obj, (_, value) => {
|
||||||
@@ -28,7 +29,7 @@ function jsonEqual<A, B>(a: A, b: B): boolean {
|
|||||||
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
class Store<T extends Record<string, any>> {
|
class StoreService<T extends Record<string, any>> {
|
||||||
private value: T;
|
private value: T;
|
||||||
private readonly initial: T;
|
private readonly initial: T;
|
||||||
private listeners = new Set<Listener<T>>();
|
private listeners = new Set<Listener<T>>();
|
||||||
@@ -61,9 +62,17 @@ class Store<T extends Record<string, any>> {
|
|||||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(): void {
|
reset<K extends keyof T>(keys?: K[]): void {
|
||||||
const prev = this.value;
|
const prev = this.value;
|
||||||
const next = structuredClone(this.initial);
|
const next = structuredClone(this.value);
|
||||||
|
|
||||||
|
if (keys && keys.length > 0) {
|
||||||
|
keys.forEach((key) => {
|
||||||
|
next[key] = structuredClone(this.initial[key]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.assign(next, structuredClone(this.initial));
|
||||||
|
}
|
||||||
|
|
||||||
if (jsonEqual(prev, next)) return;
|
if (jsonEqual(prev, next)) return;
|
||||||
|
|
||||||
@@ -112,6 +121,21 @@ class Store<T extends Record<string, any>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDiagnosticsChecksItem {
|
||||||
|
state: 'error' | 'warning' | 'success';
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDiagnosticsChecksStoreItem {
|
||||||
|
order: number;
|
||||||
|
code: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
state: 'loading' | 'warning' | 'success' | 'error' | 'skipped';
|
||||||
|
items: Array<IDiagnosticsChecksItem>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StoreType {
|
export interface StoreType {
|
||||||
tabService: {
|
tabService: {
|
||||||
current: string;
|
current: string;
|
||||||
@@ -143,6 +167,29 @@ export interface StoreType {
|
|||||||
data: Podkop.OutboundGroup[];
|
data: Podkop.OutboundGroup[];
|
||||||
latencyFetching: boolean;
|
latencyFetching: boolean;
|
||||||
};
|
};
|
||||||
|
diagnosticsRunAction: {
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
|
diagnosticsChecks: Array<IDiagnosticsChecksStoreItem>;
|
||||||
|
diagnosticsActions: {
|
||||||
|
restart: { loading: boolean };
|
||||||
|
start: { loading: boolean };
|
||||||
|
stop: { loading: boolean };
|
||||||
|
enable: { loading: boolean };
|
||||||
|
disable: { loading: boolean };
|
||||||
|
globalCheck: { loading: boolean };
|
||||||
|
viewLogs: { loading: boolean };
|
||||||
|
showSingBoxConfig: { loading: boolean };
|
||||||
|
};
|
||||||
|
diagnosticsSystemInfo: {
|
||||||
|
loading: boolean;
|
||||||
|
podkop_version: string;
|
||||||
|
podkop_latest_version: string;
|
||||||
|
luci_app_version: string;
|
||||||
|
sing_box_version: string;
|
||||||
|
openwrt_version: string;
|
||||||
|
device_model: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialStore: StoreType = {
|
const initialStore: StoreType = {
|
||||||
@@ -176,6 +223,7 @@ const initialStore: StoreType = {
|
|||||||
latencyFetching: false,
|
latencyFetching: false,
|
||||||
data: [],
|
data: [],
|
||||||
},
|
},
|
||||||
|
...initialDiagnosticStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const store = new Store<StoreType>(initialStore);
|
export const store = new StoreService<StoreType>(initialStore);
|
||||||
@@ -1,2 +1,9 @@
|
|||||||
export * from './renderDashboard';
|
import { render } from './render';
|
||||||
export * from './initDashboardController';
|
import { initController } from './initController';
|
||||||
|
import { styles } from './styles';
|
||||||
|
|
||||||
|
export const DashboardTab = {
|
||||||
|
render,
|
||||||
|
initController,
|
||||||
|
styles,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
getDashboardSections,
|
|
||||||
getPodkopStatus,
|
|
||||||
getSingboxStatus,
|
|
||||||
} from '../../methods';
|
|
||||||
import {
|
|
||||||
getClashApiUrl,
|
|
||||||
getClashWsUrl,
|
getClashWsUrl,
|
||||||
onMount,
|
onMount,
|
||||||
preserveScrollForPage,
|
preserveScrollForPage,
|
||||||
} from '../../../helpers';
|
} from '../../../helpers';
|
||||||
import {
|
|
||||||
triggerLatencyGroupTest,
|
|
||||||
triggerLatencyProxyTest,
|
|
||||||
triggerProxySelector,
|
|
||||||
} from '../../../clash';
|
|
||||||
import { store, StoreType } from '../../../store';
|
|
||||||
import { socket } from '../../../socket';
|
|
||||||
import { prettyBytes } from '../../../helpers/prettyBytes';
|
import { prettyBytes } from '../../../helpers/prettyBytes';
|
||||||
import { renderSections } from './renderSections';
|
import { CustomPodkopMethods, PodkopShellMethods } from '../../methods';
|
||||||
import { renderWidget } from './renderWidget';
|
import { logger, socket, store, StoreType } from '../../services';
|
||||||
|
import { renderSections, renderWidget } from './partials';
|
||||||
|
import { fetchServicesInfo } from '../../fetchers';
|
||||||
|
import { getClashApiSecret } from '../../methods/custom/getClashApiSecret';
|
||||||
|
|
||||||
// Fetchers
|
// Fetchers
|
||||||
|
|
||||||
@@ -32,10 +22,10 @@ async function fetchDashboardSections() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, success } = await getDashboardSections();
|
const { data, success } = await CustomPodkopMethods.getDashboardSections();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
console.log('[fetchDashboardSections]: failed to fetch', getClashApiUrl());
|
logger.error('[DASHBOARD]', 'fetchDashboardSections: failed to fetch');
|
||||||
}
|
}
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
@@ -48,36 +38,11 @@ async function fetchDashboardSections() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchServicesInfo() {
|
|
||||||
try {
|
|
||||||
const [podkop, singbox] = await Promise.all([
|
|
||||||
getPodkopStatus(),
|
|
||||||
getSingboxStatus(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
store.set({
|
|
||||||
servicesInfoWidget: {
|
|
||||||
loading: false,
|
|
||||||
failed: false,
|
|
||||||
data: { singbox: singbox.running, podkop: podkop.enabled },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log('[fetchServicesInfo]: failed to fetchServices', err);
|
|
||||||
|
|
||||||
store.set({
|
|
||||||
servicesInfoWidget: {
|
|
||||||
loading: false,
|
|
||||||
failed: true,
|
|
||||||
data: { singbox: 0, podkop: 0 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectToClashSockets() {
|
async function connectToClashSockets() {
|
||||||
|
const clashApiSecret = await getClashApiSecret();
|
||||||
|
|
||||||
socket.subscribe(
|
socket.subscribe(
|
||||||
`${getClashWsUrl()}/traffic?token=`,
|
`${getClashWsUrl()}/traffic?token=${clashApiSecret}`,
|
||||||
(msg) => {
|
(msg) => {
|
||||||
const parsedMsg = JSON.parse(msg);
|
const parsedMsg = JSON.parse(msg);
|
||||||
|
|
||||||
@@ -90,8 +55,9 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
console.log(
|
logger.error(
|
||||||
'[fetchDashboardSections]: failed to connect',
|
'[DASHBOARD]',
|
||||||
|
'connectToClashSockets - traffic: failed to connect to',
|
||||||
getClashWsUrl(),
|
getClashWsUrl(),
|
||||||
);
|
);
|
||||||
store.set({
|
store.set({
|
||||||
@@ -105,7 +71,7 @@ async function connectToClashSockets() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
socket.subscribe(
|
socket.subscribe(
|
||||||
`${getClashWsUrl()}/connections?token=`,
|
`${getClashWsUrl()}/connections?token=${clashApiSecret}`,
|
||||||
(msg) => {
|
(msg) => {
|
||||||
const parsedMsg = JSON.parse(msg);
|
const parsedMsg = JSON.parse(msg);
|
||||||
|
|
||||||
@@ -129,8 +95,9 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
console.log(
|
logger.error(
|
||||||
'[fetchDashboardSections]: failed to connect',
|
'[DASHBOARD]',
|
||||||
|
'connectToClashSockets - connections: failed to connect to',
|
||||||
getClashWsUrl(),
|
getClashWsUrl(),
|
||||||
);
|
);
|
||||||
store.set({
|
store.set({
|
||||||
@@ -155,7 +122,7 @@ async function connectToClashSockets() {
|
|||||||
// Handlers
|
// Handlers
|
||||||
|
|
||||||
async function handleChooseOutbound(selector: string, tag: string) {
|
async function handleChooseOutbound(selector: string, tag: string) {
|
||||||
await triggerProxySelector(selector, tag);
|
await PodkopShellMethods.setClashApiGroupProxy(selector, tag);
|
||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +134,7 @@ async function handleTestGroupLatency(tag: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggerLatencyGroupTest(tag);
|
await PodkopShellMethods.getClashApiGroupLatency(tag);
|
||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
@@ -186,7 +153,7 @@ async function handleTestProxyLatency(tag: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggerLatencyProxyTest(tag);
|
await PodkopShellMethods.getClashApiProxyLatency(tag);
|
||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
@@ -200,7 +167,7 @@ async function handleTestProxyLatency(tag: string) {
|
|||||||
// Renderer
|
// Renderer
|
||||||
|
|
||||||
async function renderSectionsWidget() {
|
async function renderSectionsWidget() {
|
||||||
console.log('renderSectionsWidget');
|
logger.debug('[DASHBOARD]', 'renderSectionsWidget');
|
||||||
const sectionsWidget = store.get().sectionsWidget;
|
const sectionsWidget = store.get().sectionsWidget;
|
||||||
const container = document.getElementById('dashboard-sections-grid');
|
const container = document.getElementById('dashboard-sections-grid');
|
||||||
|
|
||||||
@@ -249,7 +216,7 @@ async function renderSectionsWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderBandwidthWidget() {
|
async function renderBandwidthWidget() {
|
||||||
console.log('renderBandwidthWidget');
|
logger.debug('[DASHBOARD]', 'renderBandwidthWidget');
|
||||||
const traffic = store.get().bandwidthWidget;
|
const traffic = store.get().bandwidthWidget;
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-widget-traffic');
|
const container = document.getElementById('dashboard-widget-traffic');
|
||||||
@@ -279,7 +246,7 @@ async function renderBandwidthWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderTrafficTotalWidget() {
|
async function renderTrafficTotalWidget() {
|
||||||
console.log('renderTrafficTotalWidget');
|
logger.debug('[DASHBOARD]', 'renderTrafficTotalWidget');
|
||||||
const trafficTotalWidget = store.get().trafficTotalWidget;
|
const trafficTotalWidget = store.get().trafficTotalWidget;
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-widget-traffic-total');
|
const container = document.getElementById('dashboard-widget-traffic-total');
|
||||||
@@ -315,7 +282,7 @@ async function renderTrafficTotalWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderSystemInfoWidget() {
|
async function renderSystemInfoWidget() {
|
||||||
console.log('renderSystemInfoWidget');
|
logger.debug('[DASHBOARD]', 'renderSystemInfoWidget');
|
||||||
const systemInfoWidget = store.get().systemInfoWidget;
|
const systemInfoWidget = store.get().systemInfoWidget;
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-widget-system-info');
|
const container = document.getElementById('dashboard-widget-system-info');
|
||||||
@@ -351,7 +318,7 @@ async function renderSystemInfoWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderServicesInfoWidget() {
|
async function renderServicesInfoWidget() {
|
||||||
console.log('renderServicesInfoWidget');
|
logger.debug('[DASHBOARD]', 'renderServicesInfoWidget');
|
||||||
const servicesInfoWidget = store.get().servicesInfoWidget;
|
const servicesInfoWidget = store.get().servicesInfoWidget;
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-widget-service-info');
|
const container = document.getElementById('dashboard-widget-service-info');
|
||||||
@@ -426,19 +393,71 @@ async function onStoreUpdate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initDashboardController(): Promise<void> {
|
async function onPageMount() {
|
||||||
onMount('dashboard-status').then(() => {
|
// Cleanup before mount
|
||||||
// Remove old listener
|
onPageUnmount();
|
||||||
store.unsubscribe(onStoreUpdate);
|
|
||||||
// Clear store
|
|
||||||
store.reset();
|
|
||||||
|
|
||||||
// Add new listener
|
// Add new listener
|
||||||
store.subscribe(onStoreUpdate);
|
store.subscribe(onStoreUpdate);
|
||||||
|
|
||||||
// Initial sections fetch
|
// Initial sections fetch
|
||||||
fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
fetchServicesInfo();
|
await fetchServicesInfo();
|
||||||
connectToClashSockets();
|
await connectToClashSockets();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageUnmount() {
|
||||||
|
// Remove old listener
|
||||||
|
store.unsubscribe(onStoreUpdate);
|
||||||
|
// Clear store
|
||||||
|
store.reset([
|
||||||
|
'bandwidthWidget',
|
||||||
|
'trafficTotalWidget',
|
||||||
|
'systemInfoWidget',
|
||||||
|
'servicesInfoWidget',
|
||||||
|
'sectionsWidget',
|
||||||
|
]);
|
||||||
|
socket.resetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerLifecycleListeners() {
|
||||||
|
store.subscribe((next, prev, diff) => {
|
||||||
|
if (
|
||||||
|
diff.tabService &&
|
||||||
|
next.tabService.current !== prev.tabService.current
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
'[DASHBOARD]',
|
||||||
|
'active tab diff event, active tab:',
|
||||||
|
diff.tabService.current,
|
||||||
|
);
|
||||||
|
const isDashboardVisible = next.tabService.current === 'dashboard';
|
||||||
|
|
||||||
|
if (isDashboardVisible) {
|
||||||
|
logger.debug(
|
||||||
|
'[DASHBOARD]',
|
||||||
|
'registerLifecycleListeners',
|
||||||
|
'onPageMount',
|
||||||
|
);
|
||||||
|
return onPageMount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDashboardVisible) {
|
||||||
|
logger.debug(
|
||||||
|
'[DASHBOARD]',
|
||||||
|
'registerLifecycleListeners',
|
||||||
|
'onPageUnmount',
|
||||||
|
);
|
||||||
|
return onPageUnmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initController(): Promise<void> {
|
||||||
|
onMount('dashboard-status').then(() => {
|
||||||
|
logger.debug('[DASHBOARD]', 'initController', 'onMount');
|
||||||
|
onPageMount();
|
||||||
|
registerLifecycleListeners();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './renderSections';
|
||||||
|
export * from './renderWidget';
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Podkop } from '../../types';
|
import { Podkop } from '../../../types';
|
||||||
import { getClashApiUrl } from '../../../helpers';
|
|
||||||
|
|
||||||
interface IRenderSectionsProps {
|
interface IRenderSectionsProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -17,10 +16,7 @@ function renderFailedState() {
|
|||||||
class: 'pdk_dashboard-page__outbound-section centered',
|
class: 'pdk_dashboard-page__outbound-section centered',
|
||||||
style: 'height: 127px',
|
style: 'height: 127px',
|
||||||
},
|
},
|
||||||
E('span', {}, [
|
E('span', {}, [E('span', {}, _('Dashboard currently unavailable'))]),
|
||||||
E('span', {}, _('Dashboard currently unavailable')),
|
|
||||||
E('div', { style: 'text-align: center;' }, `API: ${getClashApiUrl()}`),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { renderSections } from './renderSections';
|
import { renderSections, renderWidget } from './partials';
|
||||||
import { renderWidget } from './renderWidget';
|
|
||||||
|
|
||||||
export function renderDashboard() {
|
export function render() {
|
||||||
return E(
|
return E(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
@@ -47,6 +46,7 @@ export function renderDashboard() {
|
|||||||
},
|
},
|
||||||
onTestLatency: () => {},
|
onTestLatency: () => {},
|
||||||
onChooseOutbound: () => {},
|
onChooseOutbound: () => {},
|
||||||
|
latencyFetching: false,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
120
fe-app-podkop/src/podkop/tabs/dashboard/styles.ts
Normal file
120
fe-app-podkop/src/podkop/tabs/dashboard/styles.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// language=CSS
|
||||||
|
export const styles = `
|
||||||
|
#cbi-podkop-dashboard-_mount_node > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cbi-podkop-dashboard > h3 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page {
|
||||||
|
width: 100%;
|
||||||
|
--dashboard-grid-columns: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.pdk_dashboard-page {
|
||||||
|
--dashboard-grid-columns: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item {
|
||||||
|
border: 2px var(--background-color-low, lightgray) solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__title {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value {
|
||||||
|
color: var(--success-color-medium, green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value {
|
||||||
|
color: var(--error-color-medium, red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row__key {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row__value {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-section {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 2px var(--background-color-low, lightgray) solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-section__title-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-section__title-section__title {
|
||||||
|
color: var(--text-color-high);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item {
|
||||||
|
border: 2px var(--background-color-low, lightgray) solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
transition: border 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item--selectable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item--selectable:hover {
|
||||||
|
border-color: var(--primary-color-high, dodgerblue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item--active {
|
||||||
|
border-color: var(--success-color-medium, green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__type {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__latency--empty {
|
||||||
|
color: var(--primary-color-low, lightgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__latency--green {
|
||||||
|
color: var(--success-color-medium, green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__latency--yellow {
|
||||||
|
color: var(--warn-color-medium, orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__latency--red {
|
||||||
|
color: var(--error-color-medium, red);
|
||||||
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { getCheckTitle } from '../helpers/getCheckTitle';
|
||||||
|
|
||||||
|
export enum DIAGNOSTICS_CHECKS {
|
||||||
|
DNS = 'DNS',
|
||||||
|
SINGBOX = 'SINGBOX',
|
||||||
|
NFT = 'NFT',
|
||||||
|
FAKEIP = 'FAKEIP',
|
||||||
|
OUTBOUNDS = 'OUTBOUNDS',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DIAGNOSTICS_CHECKS_MAP: Record<
|
||||||
|
DIAGNOSTICS_CHECKS,
|
||||||
|
{ order: number; title: string; code: DIAGNOSTICS_CHECKS }
|
||||||
|
> = {
|
||||||
|
[DIAGNOSTICS_CHECKS.DNS]: {
|
||||||
|
order: 1,
|
||||||
|
title: getCheckTitle('DNS'),
|
||||||
|
code: DIAGNOSTICS_CHECKS.DNS,
|
||||||
|
},
|
||||||
|
[DIAGNOSTICS_CHECKS.SINGBOX]: {
|
||||||
|
order: 2,
|
||||||
|
title: getCheckTitle('Sing-box'),
|
||||||
|
code: DIAGNOSTICS_CHECKS.SINGBOX,
|
||||||
|
},
|
||||||
|
[DIAGNOSTICS_CHECKS.NFT]: {
|
||||||
|
order: 3,
|
||||||
|
title: getCheckTitle('Nftables'),
|
||||||
|
code: DIAGNOSTICS_CHECKS.NFT,
|
||||||
|
},
|
||||||
|
[DIAGNOSTICS_CHECKS.OUTBOUNDS]: {
|
||||||
|
order: 4,
|
||||||
|
title: getCheckTitle('Outbounds'),
|
||||||
|
code: DIAGNOSTICS_CHECKS.OUTBOUNDS,
|
||||||
|
},
|
||||||
|
[DIAGNOSTICS_CHECKS.FAKEIP]: {
|
||||||
|
order: 5,
|
||||||
|
title: getCheckTitle('FakeIP'),
|
||||||
|
code: DIAGNOSTICS_CHECKS.FAKEIP,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { insertIf } from '../../../../helpers';
|
||||||
|
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||||
|
import { PodkopShellMethods } from '../../../methods';
|
||||||
|
import { IDiagnosticsChecksItem } from '../../../services';
|
||||||
|
import { updateCheckStore } from './updateCheckStore';
|
||||||
|
import { getMeta } from '../helpers/getMeta';
|
||||||
|
|
||||||
|
export async function runDnsCheck() {
|
||||||
|
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS;
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Checking, please wait'),
|
||||||
|
state: 'loading',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dnsChecks = await PodkopShellMethods.checkDNSAvailable();
|
||||||
|
|
||||||
|
if (!dnsChecks.success) {
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Cannot receive checks result'),
|
||||||
|
state: 'error',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error('DNS checks failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = dnsChecks.data;
|
||||||
|
|
||||||
|
const allGood =
|
||||||
|
Boolean(data.dns_on_router) &&
|
||||||
|
Boolean(data.dhcp_config_status) &&
|
||||||
|
Boolean(data.bootstrap_dns_status) &&
|
||||||
|
Boolean(data.dns_status);
|
||||||
|
|
||||||
|
const atLeastOneGood =
|
||||||
|
Boolean(data.dns_on_router) ||
|
||||||
|
Boolean(data.dhcp_config_status) ||
|
||||||
|
Boolean(data.bootstrap_dns_status) ||
|
||||||
|
Boolean(data.dns_status);
|
||||||
|
|
||||||
|
const { state, description } = getMeta({ atLeastOneGood, allGood });
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
items: [
|
||||||
|
...insertIf<IDiagnosticsChecksItem>(
|
||||||
|
data.dns_type === 'doh' ||
|
||||||
|
data.dns_type === 'dot' ||
|
||||||
|
!data.bootstrap_dns_status,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
state: data.bootstrap_dns_status ? 'success' : 'error',
|
||||||
|
key: _('Bootsrap DNS'),
|
||||||
|
value: data.bootstrap_dns_server,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
{
|
||||||
|
state: data.dns_status ? 'success' : 'error',
|
||||||
|
key: _('Main DNS'),
|
||||||
|
value: `${data.dns_server} [${data.dns_type}]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.dns_on_router ? 'success' : 'error',
|
||||||
|
key: _('DNS on router'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.dhcp_config_status ? 'success' : 'error',
|
||||||
|
key: _('DHCP has DNS server'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!atLeastOneGood) {
|
||||||
|
throw new Error('DNS checks failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { insertIf } from '../../../../helpers';
|
||||||
|
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||||
|
import { PodkopShellMethods, RemoteFakeIPMethods } from '../../../methods';
|
||||||
|
import { IDiagnosticsChecksItem } from '../../../services';
|
||||||
|
import { updateCheckStore } from './updateCheckStore';
|
||||||
|
import { getMeta } from '../helpers/getMeta';
|
||||||
|
|
||||||
|
export async function runFakeIPCheck() {
|
||||||
|
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP;
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Checking, please wait'),
|
||||||
|
state: 'loading',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const routerFakeIPResponse = await PodkopShellMethods.checkFakeIP();
|
||||||
|
const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck();
|
||||||
|
const checkIPResponse = await RemoteFakeIPMethods.getIpCheck();
|
||||||
|
|
||||||
|
const checks = {
|
||||||
|
router: routerFakeIPResponse.success && routerFakeIPResponse.data.fakeip,
|
||||||
|
browserFakeIP:
|
||||||
|
checkFakeIPResponse.success && checkFakeIPResponse.data.fakeip,
|
||||||
|
differentIP:
|
||||||
|
checkFakeIPResponse.success &&
|
||||||
|
checkIPResponse.success &&
|
||||||
|
checkFakeIPResponse.data.IP !== checkIPResponse.data.IP,
|
||||||
|
};
|
||||||
|
|
||||||
|
const allGood = checks.router || checks.browserFakeIP || checks.differentIP;
|
||||||
|
const atLeastOneGood =
|
||||||
|
checks.router && checks.browserFakeIP && checks.differentIP;
|
||||||
|
|
||||||
|
const { state, description } = getMeta({ atLeastOneGood, allGood });
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
state: checks.router ? 'success' : 'warning',
|
||||||
|
key: checks.router
|
||||||
|
? _('Router DNS is routed through sing-box')
|
||||||
|
: _('Router DNS is not routed through sing-box'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: checks.browserFakeIP ? 'success' : 'error',
|
||||||
|
key: checks.browserFakeIP
|
||||||
|
? _('Browser is using FakeIP correctly')
|
||||||
|
: _('Browser is not using FakeIP'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
...insertIf<IDiagnosticsChecksItem>(checks.browserFakeIP, [
|
||||||
|
{
|
||||||
|
state: checks.differentIP ? 'success' : 'error',
|
||||||
|
key: checks.differentIP
|
||||||
|
? _('Proxy traffic is routed via FakeIP')
|
||||||
|
: _('Proxy traffic is not routed via FakeIP'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
115
fe-app-podkop/src/podkop/tabs/diagnostic/checks/runNftCheck.ts
Normal file
115
fe-app-podkop/src/podkop/tabs/diagnostic/checks/runNftCheck.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||||
|
import { RemoteFakeIPMethods, PodkopShellMethods } from '../../../methods';
|
||||||
|
import { updateCheckStore } from './updateCheckStore';
|
||||||
|
import { getMeta } from '../helpers/getMeta';
|
||||||
|
|
||||||
|
export async function runNftCheck() {
|
||||||
|
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT;
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Checking, please wait'),
|
||||||
|
state: 'loading',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await RemoteFakeIPMethods.getFakeIpCheck();
|
||||||
|
await RemoteFakeIPMethods.getIpCheck();
|
||||||
|
|
||||||
|
const nftablesChecks = await PodkopShellMethods.checkNftRules();
|
||||||
|
|
||||||
|
if (!nftablesChecks.success) {
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Cannot receive checks result'),
|
||||||
|
state: 'error',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error('Nftables checks failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = nftablesChecks.data;
|
||||||
|
|
||||||
|
const allGood =
|
||||||
|
Boolean(data.table_exist) &&
|
||||||
|
Boolean(data.rules_mangle_exist) &&
|
||||||
|
Boolean(data.rules_mangle_counters) &&
|
||||||
|
Boolean(data.rules_mangle_output_exist) &&
|
||||||
|
Boolean(data.rules_mangle_output_counters) &&
|
||||||
|
Boolean(data.rules_proxy_exist) &&
|
||||||
|
Boolean(data.rules_proxy_counters) &&
|
||||||
|
!data.rules_other_mark_exist;
|
||||||
|
|
||||||
|
const atLeastOneGood =
|
||||||
|
Boolean(data.table_exist) ||
|
||||||
|
Boolean(data.rules_mangle_exist) ||
|
||||||
|
Boolean(data.rules_mangle_counters) ||
|
||||||
|
Boolean(data.rules_mangle_output_exist) ||
|
||||||
|
Boolean(data.rules_mangle_output_counters) ||
|
||||||
|
Boolean(data.rules_proxy_exist) ||
|
||||||
|
Boolean(data.rules_proxy_counters) ||
|
||||||
|
!data.rules_other_mark_exist;
|
||||||
|
|
||||||
|
const { state, description } = getMeta({ atLeastOneGood, allGood });
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
state: data.table_exist ? 'success' : 'error',
|
||||||
|
key: _('Table exist'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.rules_mangle_exist ? 'success' : 'error',
|
||||||
|
key: _('Rules mangle exist'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.rules_mangle_counters ? 'success' : 'error',
|
||||||
|
key: _('Rules mangle counters'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.rules_mangle_output_exist ? 'success' : 'error',
|
||||||
|
key: _('Rules mangle output exist'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.rules_mangle_output_counters ? 'success' : 'error',
|
||||||
|
key: _('Rules mangle output counters'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.rules_proxy_exist ? 'success' : 'error',
|
||||||
|
key: _('Rules proxy exist'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.rules_proxy_counters ? 'success' : 'error',
|
||||||
|
key: _('Rules proxy counters'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: !data.rules_other_mark_exist ? 'success' : 'warning',
|
||||||
|
key: !data.rules_other_mark_exist
|
||||||
|
? _('No other marking rules found')
|
||||||
|
: _('Additional marking rules found'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!atLeastOneGood) {
|
||||||
|
throw new Error('Nftables checks failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||||
|
import { PodkopShellMethods } from '../../../methods';
|
||||||
|
import { updateCheckStore } from './updateCheckStore';
|
||||||
|
import { getMeta } from '../helpers/getMeta';
|
||||||
|
import { getDashboardSections } from '../../../methods/custom/getDashboardSections';
|
||||||
|
import { IDiagnosticsChecksItem } from '../../../services';
|
||||||
|
|
||||||
|
export async function runSectionsCheck() {
|
||||||
|
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS;
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Checking, please wait'),
|
||||||
|
state: 'loading',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sections = await getDashboardSections();
|
||||||
|
|
||||||
|
if (!sections.success) {
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Cannot receive checks result'),
|
||||||
|
state: 'error',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error('Sections checks failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = (await Promise.all(
|
||||||
|
sections.data.map(async (section) => {
|
||||||
|
async function getLatency() {
|
||||||
|
if (section.withTagSelect) {
|
||||||
|
const latencyGroup = await PodkopShellMethods.getClashApiGroupLatency(
|
||||||
|
section.code,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedOutbound = section.outbounds.find(
|
||||||
|
(item) => item.selected,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isUrlTest = selectedOutbound?.type === 'URLTest';
|
||||||
|
|
||||||
|
const success = latencyGroup.success && !latencyGroup.data.message;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
if (isUrlTest) {
|
||||||
|
const latency = Object.values(latencyGroup.data)
|
||||||
|
.map((item) => (item ? `${item}ms` : 'n/a'))
|
||||||
|
.join(' / ');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
latency: `[${_('Fastest')}] ${latency}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProxyDelay =
|
||||||
|
latencyGroup.data?.[selectedOutbound?.code ?? ''];
|
||||||
|
|
||||||
|
if (selectedProxyDelay) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
latency: `[${selectedOutbound?.displayName ?? ''}] ${selectedProxyDelay}ms`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
latency: `[${selectedOutbound?.displayName ?? ''}] ${_('Not responding')}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
latency: _('Not responding'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const latencyProxy = await PodkopShellMethods.getClashApiProxyLatency(
|
||||||
|
section.code,
|
||||||
|
);
|
||||||
|
|
||||||
|
const success = latencyProxy.success && !latencyProxy.data.message;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
latency: `${latencyProxy.data.delay} ms`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
latency: _('Not responding'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { latency, success } = await getLatency();
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: success ? 'success' : 'error',
|
||||||
|
key: section.displayName,
|
||||||
|
value: latency,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
)) as Array<IDiagnosticsChecksItem>;
|
||||||
|
|
||||||
|
const allGood = items.every((item) => item.state === 'success');
|
||||||
|
|
||||||
|
const atLeastOneGood = items.some((item) => item.state === 'success');
|
||||||
|
|
||||||
|
const { state, description } = getMeta({ atLeastOneGood, allGood });
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
items,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!atLeastOneGood) {
|
||||||
|
throw new Error('Sections checks failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||||
|
import { PodkopShellMethods } from '../../../methods';
|
||||||
|
import { updateCheckStore } from './updateCheckStore';
|
||||||
|
import { getMeta } from '../helpers/getMeta';
|
||||||
|
|
||||||
|
export async function runSingBoxCheck() {
|
||||||
|
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX;
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Checking, please wait'),
|
||||||
|
state: 'loading',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const singBoxChecks = await PodkopShellMethods.checkSingBox();
|
||||||
|
|
||||||
|
if (!singBoxChecks.success) {
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description: _('Cannot receive checks result'),
|
||||||
|
state: 'error',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error('Sing-box checks failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = singBoxChecks.data;
|
||||||
|
|
||||||
|
const allGood =
|
||||||
|
Boolean(data.sing_box_installed) &&
|
||||||
|
Boolean(data.sing_box_version_ok) &&
|
||||||
|
Boolean(data.sing_box_service_exist) &&
|
||||||
|
Boolean(data.sing_box_autostart_disabled) &&
|
||||||
|
Boolean(data.sing_box_process_running) &&
|
||||||
|
Boolean(data.sing_box_ports_listening);
|
||||||
|
|
||||||
|
const atLeastOneGood =
|
||||||
|
Boolean(data.sing_box_installed) ||
|
||||||
|
Boolean(data.sing_box_version_ok) ||
|
||||||
|
Boolean(data.sing_box_service_exist) ||
|
||||||
|
Boolean(data.sing_box_autostart_disabled) ||
|
||||||
|
Boolean(data.sing_box_process_running) ||
|
||||||
|
Boolean(data.sing_box_ports_listening);
|
||||||
|
|
||||||
|
const { state, description } = getMeta({ atLeastOneGood, allGood });
|
||||||
|
|
||||||
|
updateCheckStore({
|
||||||
|
order,
|
||||||
|
code,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
state: data.sing_box_installed ? 'success' : 'error',
|
||||||
|
key: _('Sing-box installed'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.sing_box_version_ok ? 'success' : 'error',
|
||||||
|
key: _('Sing-box version is compatible (newer than 1.12.4)'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.sing_box_service_exist ? 'success' : 'error',
|
||||||
|
key: _('Sing-box service exist'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.sing_box_autostart_disabled ? 'success' : 'error',
|
||||||
|
key: _('Sing-box autostart disabled'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.sing_box_process_running ? 'success' : 'error',
|
||||||
|
key: _('Sing-box process running'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: data.sing_box_ports_listening ? 'success' : 'error',
|
||||||
|
key: _('Sing-box listening ports'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!atLeastOneGood || !data.sing_box_process_running) {
|
||||||
|
throw new Error('Sing-box checks failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { IDiagnosticsChecksStoreItem, store } from '../../../services';
|
||||||
|
|
||||||
|
export function updateCheckStore(
|
||||||
|
check: IDiagnosticsChecksStoreItem,
|
||||||
|
minified?: boolean,
|
||||||
|
) {
|
||||||
|
const diagnosticsChecks = store.get().diagnosticsChecks;
|
||||||
|
const other = diagnosticsChecks.filter((item) => item.code !== check.code);
|
||||||
|
|
||||||
|
const smallCheck: IDiagnosticsChecksStoreItem = {
|
||||||
|
...check,
|
||||||
|
items: check.items.filter((item) => item.state !== 'success'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetCheck = minified ? smallCheck : check;
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
diagnosticsChecks: [...other, targetCheck],
|
||||||
|
});
|
||||||
|
}
|
||||||
140
fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts
Normal file
140
fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
DIAGNOSTICS_CHECKS,
|
||||||
|
DIAGNOSTICS_CHECKS_MAP,
|
||||||
|
} from './checks/contstants';
|
||||||
|
import { StoreType } from '../../services';
|
||||||
|
|
||||||
|
export const initialDiagnosticStore: Pick<
|
||||||
|
StoreType,
|
||||||
|
| 'diagnosticsChecks'
|
||||||
|
| 'diagnosticsRunAction'
|
||||||
|
| 'diagnosticsActions'
|
||||||
|
| 'diagnosticsSystemInfo'
|
||||||
|
> = {
|
||||||
|
diagnosticsSystemInfo: {
|
||||||
|
loading: true,
|
||||||
|
podkop_version: 'loading',
|
||||||
|
podkop_latest_version: 'loading',
|
||||||
|
luci_app_version: 'loading',
|
||||||
|
sing_box_version: 'loading',
|
||||||
|
openwrt_version: 'loading',
|
||||||
|
device_model: 'loading',
|
||||||
|
},
|
||||||
|
diagnosticsActions: {
|
||||||
|
restart: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
stop: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
enable: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
disable: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
globalCheck: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
viewLogs: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
showSingBoxConfig: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
diagnosticsRunAction: { loading: false },
|
||||||
|
diagnosticsChecks: [
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.DNS,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.DNS.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.DNS.order,
|
||||||
|
description: _('Not running'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.SINGBOX,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order,
|
||||||
|
description: _('Not running'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.NFT,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.NFT.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.NFT.order,
|
||||||
|
description: _('Not running'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.OUTBOUNDS,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.order,
|
||||||
|
description: _('Not running'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.FAKEIP,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order,
|
||||||
|
description: _('Not running'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadingDiagnosticsChecksStore: Pick<
|
||||||
|
StoreType,
|
||||||
|
'diagnosticsChecks'
|
||||||
|
> = {
|
||||||
|
diagnosticsChecks: [
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.DNS,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.DNS.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.DNS.order,
|
||||||
|
description: _('Pending'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.SINGBOX,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order,
|
||||||
|
description: _('Pending'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.NFT,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.NFT.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.NFT.order,
|
||||||
|
description: _('Pending'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.OUTBOUNDS,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.order,
|
||||||
|
description: _('Pending'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: DIAGNOSTICS_CHECKS.FAKEIP,
|
||||||
|
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
|
||||||
|
order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order,
|
||||||
|
description: _('Pending'),
|
||||||
|
items: [],
|
||||||
|
state: 'skipped',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function getCheckTitle(name: string) {
|
||||||
|
return `${name} ${_('checks')}`;
|
||||||
|
}
|
||||||
28
fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getMeta.ts
Normal file
28
fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getMeta.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
interface IGetMetaProps {
|
||||||
|
allGood: boolean;
|
||||||
|
atLeastOneGood: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMeta({ allGood, atLeastOneGood }: IGetMetaProps): {
|
||||||
|
description: string;
|
||||||
|
state: 'loading' | 'warning' | 'success' | 'error' | 'skipped';
|
||||||
|
} {
|
||||||
|
if (allGood) {
|
||||||
|
return {
|
||||||
|
state: 'success',
|
||||||
|
description: _('Checks passed'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atLeastOneGood) {
|
||||||
|
return {
|
||||||
|
state: 'warning',
|
||||||
|
description: _('Issues detected'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: 'error',
|
||||||
|
description: _('Checks failed'),
|
||||||
|
};
|
||||||
|
}
|
||||||
9
fe-app-podkop/src/podkop/tabs/diagnostic/index.ts
Normal file
9
fe-app-podkop/src/podkop/tabs/diagnostic/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { render } from './renderDiagnostic';
|
||||||
|
import { initController } from './initController';
|
||||||
|
import { styles } from './styles';
|
||||||
|
|
||||||
|
export const DiagnosticTab = {
|
||||||
|
render,
|
||||||
|
initController,
|
||||||
|
styles,
|
||||||
|
};
|
||||||
614
fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts
Normal file
614
fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
import { onMount, preserveScrollForPage } from '../../../helpers';
|
||||||
|
import { runDnsCheck } from './checks/runDnsCheck';
|
||||||
|
import { runSingBoxCheck } from './checks/runSingBoxCheck';
|
||||||
|
import { runNftCheck } from './checks/runNftCheck';
|
||||||
|
import { runFakeIPCheck } from './checks/runFakeIPCheck';
|
||||||
|
import { loadingDiagnosticsChecksStore } from './diagnostic.store';
|
||||||
|
import { logger, store, StoreType } from '../../services';
|
||||||
|
import {
|
||||||
|
IRenderSystemInfoRow,
|
||||||
|
renderAvailableActions,
|
||||||
|
renderCheckSection,
|
||||||
|
renderRunAction,
|
||||||
|
renderSystemInfo,
|
||||||
|
} from './partials';
|
||||||
|
import { PodkopShellMethods } from '../../methods';
|
||||||
|
import { fetchServicesInfo } from '../../fetchers';
|
||||||
|
import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion';
|
||||||
|
import { renderModal } from '../../../partials';
|
||||||
|
import { PODKOP_LUCI_APP_VERSION } from '../../../constants';
|
||||||
|
import { showToast } from '../../../helpers/showToast';
|
||||||
|
import { renderWikiDisclaimer } from './partials/renderWikiDisclaimer';
|
||||||
|
import { runSectionsCheck } from './checks/runSectionsCheck';
|
||||||
|
|
||||||
|
async function fetchSystemInfo() {
|
||||||
|
const systemInfo = await PodkopShellMethods.getSystemInfo();
|
||||||
|
|
||||||
|
if (systemInfo.success) {
|
||||||
|
store.set({
|
||||||
|
diagnosticsSystemInfo: {
|
||||||
|
loading: false,
|
||||||
|
...systemInfo.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
store.set({
|
||||||
|
diagnosticsSystemInfo: {
|
||||||
|
loading: false,
|
||||||
|
podkop_version: _('unknown'),
|
||||||
|
podkop_latest_version: _('unknown'),
|
||||||
|
luci_app_version: _('unknown'),
|
||||||
|
sing_box_version: _('unknown'),
|
||||||
|
openwrt_version: _('unknown'),
|
||||||
|
device_model: _('unknown'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiagnosticsChecks() {
|
||||||
|
logger.debug('[DIAGNOSTIC]', 'renderDiagnosticsChecks');
|
||||||
|
const diagnosticsChecks = store
|
||||||
|
.get()
|
||||||
|
.diagnosticsChecks.sort((a, b) => a.order - b.order);
|
||||||
|
const container = document.getElementById('pdk_diagnostic-page-checks');
|
||||||
|
|
||||||
|
const renderedDiagnosticsChecks = diagnosticsChecks.map((check) =>
|
||||||
|
renderCheckSection(check),
|
||||||
|
);
|
||||||
|
|
||||||
|
return preserveScrollForPage(() => {
|
||||||
|
container!.replaceChildren(...renderedDiagnosticsChecks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiagnosticRunActionWidget() {
|
||||||
|
logger.debug('[DIAGNOSTIC]', 'renderDiagnosticRunActionWidget');
|
||||||
|
|
||||||
|
const { loading } = store.get().diagnosticsRunAction;
|
||||||
|
const container = document.getElementById('pdk_diagnostic-page-run-check');
|
||||||
|
|
||||||
|
const renderedAction = renderRunAction({
|
||||||
|
loading,
|
||||||
|
click: () => runChecks(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return preserveScrollForPage(() => {
|
||||||
|
container!.replaceChildren(renderedAction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRestart() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
restart: { loading: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PodkopShellMethods.restart();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleRestart - e', e);
|
||||||
|
} finally {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await fetchServicesInfo();
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
restart: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store.reset(['diagnosticsChecks']);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStop() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
stop: { loading: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PodkopShellMethods.stop();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleStop - e', e);
|
||||||
|
} finally {
|
||||||
|
await fetchServicesInfo();
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
stop: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store.reset(['diagnosticsChecks']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStart() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
start: { loading: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PodkopShellMethods.start();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleStart - e', e);
|
||||||
|
} finally {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await fetchServicesInfo();
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
start: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store.reset(['diagnosticsChecks']);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEnable() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
enable: { loading: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PodkopShellMethods.enable();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleEnable - e', e);
|
||||||
|
} finally {
|
||||||
|
await fetchServicesInfo();
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
enable: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDisable() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
disable: { loading: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PodkopShellMethods.disable();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleDisable - e', e);
|
||||||
|
} finally {
|
||||||
|
await fetchServicesInfo();
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
disable: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleShowGlobalCheck() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
globalCheck: { loading: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const globalCheck = await PodkopShellMethods.globalCheck();
|
||||||
|
|
||||||
|
if (globalCheck.success) {
|
||||||
|
ui.showModal(
|
||||||
|
_('Global check'),
|
||||||
|
renderModal(globalCheck.data as string, 'global_check'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleShowGlobalCheck - e', globalCheck);
|
||||||
|
showToast(_('Failed to execute!'), 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleShowGlobalCheck - e', e);
|
||||||
|
showToast(_('Failed to execute!'), 'error');
|
||||||
|
} finally {
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
globalCheck: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleViewLogs() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
viewLogs: { loading: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const viewLogs = await PodkopShellMethods.checkLogs();
|
||||||
|
|
||||||
|
if (viewLogs.success) {
|
||||||
|
ui.showModal(
|
||||||
|
_('View logs'),
|
||||||
|
renderModal(viewLogs.data as string, 'view_logs'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', viewLogs);
|
||||||
|
showToast(_('Failed to execute!'), 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', e);
|
||||||
|
showToast(_('Failed to execute!'), 'error');
|
||||||
|
} finally {
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
viewLogs: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleShowSingBoxConfig() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
showSingBoxConfig: { loading: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig();
|
||||||
|
|
||||||
|
if (showSingBoxConfig.success) {
|
||||||
|
ui.showModal(
|
||||||
|
_('Show sing-box config'),
|
||||||
|
renderModal(showSingBoxConfig.data as string, 'show_sing_box_config'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
'[DIAGNOSTIC]',
|
||||||
|
'handleShowSingBoxConfig - e',
|
||||||
|
showSingBoxConfig,
|
||||||
|
);
|
||||||
|
showToast(_('Failed to execute!'), 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'handleShowSingBoxConfig - e', e);
|
||||||
|
showToast(_('Failed to execute!'), 'error');
|
||||||
|
} finally {
|
||||||
|
store.set({
|
||||||
|
diagnosticsActions: {
|
||||||
|
...diagnosticsActions,
|
||||||
|
showSingBoxConfig: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWikiDisclaimerWidget() {
|
||||||
|
const diagnosticsChecks = store.get().diagnosticsChecks;
|
||||||
|
|
||||||
|
function getWikiKind() {
|
||||||
|
const allResults = diagnosticsChecks.map((check) => check.state);
|
||||||
|
|
||||||
|
if (allResults.includes('error')) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allResults.includes('warning')) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.getElementById('pdk_diagnostic-page-wiki');
|
||||||
|
|
||||||
|
return preserveScrollForPage(() => {
|
||||||
|
container!.replaceChildren(renderWikiDisclaimer(getWikiKind()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiagnosticAvailableActionsWidget() {
|
||||||
|
const diagnosticsActions = store.get().diagnosticsActions;
|
||||||
|
const servicesInfoWidget = store.get().servicesInfoWidget;
|
||||||
|
logger.debug('[DIAGNOSTIC]', 'renderDiagnosticAvailableActionsWidget');
|
||||||
|
|
||||||
|
const podkopEnabled = Boolean(servicesInfoWidget.data.podkop);
|
||||||
|
const singBoxRunning = Boolean(servicesInfoWidget.data.singbox);
|
||||||
|
const atLeastOneServiceCommandLoading =
|
||||||
|
servicesInfoWidget.loading ||
|
||||||
|
diagnosticsActions.restart.loading ||
|
||||||
|
diagnosticsActions.start.loading ||
|
||||||
|
diagnosticsActions.stop.loading;
|
||||||
|
|
||||||
|
const container = document.getElementById('pdk_diagnostic-page-actions');
|
||||||
|
|
||||||
|
const renderedActions = renderAvailableActions({
|
||||||
|
restart: {
|
||||||
|
loading: diagnosticsActions.restart.loading,
|
||||||
|
visible: true,
|
||||||
|
onClick: handleRestart,
|
||||||
|
disabled: atLeastOneServiceCommandLoading,
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
loading: diagnosticsActions.start.loading,
|
||||||
|
visible: !singBoxRunning,
|
||||||
|
onClick: handleStart,
|
||||||
|
disabled: atLeastOneServiceCommandLoading,
|
||||||
|
},
|
||||||
|
stop: {
|
||||||
|
loading: diagnosticsActions.stop.loading,
|
||||||
|
visible: singBoxRunning,
|
||||||
|
onClick: handleStop,
|
||||||
|
disabled: atLeastOneServiceCommandLoading,
|
||||||
|
},
|
||||||
|
enable: {
|
||||||
|
loading: diagnosticsActions.enable.loading,
|
||||||
|
visible: !podkopEnabled,
|
||||||
|
onClick: handleEnable,
|
||||||
|
disabled: atLeastOneServiceCommandLoading,
|
||||||
|
},
|
||||||
|
disable: {
|
||||||
|
loading: diagnosticsActions.disable.loading,
|
||||||
|
visible: podkopEnabled,
|
||||||
|
onClick: handleDisable,
|
||||||
|
disabled: atLeastOneServiceCommandLoading,
|
||||||
|
},
|
||||||
|
globalCheck: {
|
||||||
|
loading: diagnosticsActions.globalCheck.loading,
|
||||||
|
visible: true,
|
||||||
|
onClick: handleShowGlobalCheck,
|
||||||
|
disabled: atLeastOneServiceCommandLoading,
|
||||||
|
},
|
||||||
|
viewLogs: {
|
||||||
|
loading: diagnosticsActions.viewLogs.loading,
|
||||||
|
visible: true,
|
||||||
|
onClick: handleViewLogs,
|
||||||
|
disabled: atLeastOneServiceCommandLoading,
|
||||||
|
},
|
||||||
|
showSingBoxConfig: {
|
||||||
|
loading: diagnosticsActions.showSingBoxConfig.loading,
|
||||||
|
visible: true,
|
||||||
|
onClick: handleShowSingBoxConfig,
|
||||||
|
disabled: atLeastOneServiceCommandLoading,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return preserveScrollForPage(() => {
|
||||||
|
container!.replaceChildren(renderedActions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiagnosticSystemInfoWidget() {
|
||||||
|
logger.debug('[DIAGNOSTIC]', 'renderDiagnosticSystemInfoWidget');
|
||||||
|
const diagnosticsSystemInfo = store.get().diagnosticsSystemInfo;
|
||||||
|
|
||||||
|
const container = document.getElementById('pdk_diagnostic-page-system-info');
|
||||||
|
|
||||||
|
function getPodkopVersionRow(): IRenderSystemInfoRow {
|
||||||
|
const loading = diagnosticsSystemInfo.loading;
|
||||||
|
const unknown = diagnosticsSystemInfo.podkop_version === _('unknown');
|
||||||
|
const hasActualVersion =
|
||||||
|
Boolean(diagnosticsSystemInfo.podkop_latest_version) &&
|
||||||
|
diagnosticsSystemInfo.podkop_latest_version !== 'unknown';
|
||||||
|
const version = normalizeCompiledVersion(
|
||||||
|
diagnosticsSystemInfo.podkop_version,
|
||||||
|
);
|
||||||
|
const isDevVersion = version === 'dev';
|
||||||
|
|
||||||
|
if (loading || unknown || !hasActualVersion || isDevVersion) {
|
||||||
|
return {
|
||||||
|
key: 'Podkop',
|
||||||
|
value: version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version !== `v${diagnosticsSystemInfo.podkop_latest_version}`) {
|
||||||
|
logger.debug(
|
||||||
|
'[DIAGNOSTIC]',
|
||||||
|
'diagnosticsSystemInfo',
|
||||||
|
diagnosticsSystemInfo,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
key: 'Podkop',
|
||||||
|
value: version,
|
||||||
|
tag: {
|
||||||
|
label: _('Outdated'),
|
||||||
|
kind: 'warning',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: 'Podkop',
|
||||||
|
value: version,
|
||||||
|
tag: {
|
||||||
|
label: _('Latest'),
|
||||||
|
kind: 'success',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedSystemInfo = renderSystemInfo({
|
||||||
|
items: [
|
||||||
|
getPodkopVersionRow(),
|
||||||
|
{
|
||||||
|
key: 'Luci App',
|
||||||
|
value: normalizeCompiledVersion(PODKOP_LUCI_APP_VERSION),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Sing-box',
|
||||||
|
value: diagnosticsSystemInfo.sing_box_version,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'OS',
|
||||||
|
value: diagnosticsSystemInfo.openwrt_version,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Device',
|
||||||
|
value: diagnosticsSystemInfo.device_model,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return preserveScrollForPage(() => {
|
||||||
|
container!.replaceChildren(renderedSystemInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStoreUpdate(
|
||||||
|
next: StoreType,
|
||||||
|
prev: StoreType,
|
||||||
|
diff: Partial<StoreType>,
|
||||||
|
) {
|
||||||
|
if (diff.diagnosticsChecks) {
|
||||||
|
renderDiagnosticsChecks();
|
||||||
|
renderWikiDisclaimerWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.diagnosticsRunAction) {
|
||||||
|
renderDiagnosticRunActionWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.diagnosticsActions || diff.servicesInfoWidget) {
|
||||||
|
renderDiagnosticAvailableActionsWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.diagnosticsSystemInfo) {
|
||||||
|
renderDiagnosticSystemInfoWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChecks() {
|
||||||
|
try {
|
||||||
|
store.set({
|
||||||
|
diagnosticsRunAction: { loading: true },
|
||||||
|
diagnosticsChecks: loadingDiagnosticsChecksStore.diagnosticsChecks,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runDnsCheck();
|
||||||
|
|
||||||
|
await runSingBoxCheck();
|
||||||
|
|
||||||
|
await runNftCheck();
|
||||||
|
|
||||||
|
await runSectionsCheck();
|
||||||
|
|
||||||
|
await runFakeIPCheck();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[DIAGNOSTIC]', 'runChecks - e', e);
|
||||||
|
} finally {
|
||||||
|
store.set({ diagnosticsRunAction: { loading: false } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageMount() {
|
||||||
|
// Cleanup before mount
|
||||||
|
onPageUnmount();
|
||||||
|
|
||||||
|
// Add new listener
|
||||||
|
store.subscribe(onStoreUpdate);
|
||||||
|
|
||||||
|
// Initial checks render
|
||||||
|
renderDiagnosticsChecks();
|
||||||
|
|
||||||
|
// Initial run checks action render
|
||||||
|
renderDiagnosticRunActionWidget();
|
||||||
|
|
||||||
|
// Initial available actions render
|
||||||
|
renderDiagnosticAvailableActionsWidget();
|
||||||
|
|
||||||
|
// Initial system info render
|
||||||
|
renderDiagnosticSystemInfoWidget();
|
||||||
|
|
||||||
|
// Initial Wiki disclaimer render
|
||||||
|
renderWikiDisclaimerWidget();
|
||||||
|
|
||||||
|
// Initial services info fetch
|
||||||
|
fetchServicesInfo();
|
||||||
|
|
||||||
|
// Initial system info fetch
|
||||||
|
fetchSystemInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageUnmount() {
|
||||||
|
// Remove old listener
|
||||||
|
store.unsubscribe(onStoreUpdate);
|
||||||
|
|
||||||
|
// Clear store
|
||||||
|
store.reset([
|
||||||
|
'diagnosticsActions',
|
||||||
|
'diagnosticsSystemInfo',
|
||||||
|
'diagnosticsChecks',
|
||||||
|
'diagnosticsRunAction',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerLifecycleListeners() {
|
||||||
|
store.subscribe((next, prev, diff) => {
|
||||||
|
if (
|
||||||
|
diff.tabService &&
|
||||||
|
next.tabService.current !== prev.tabService.current
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
'[DIAGNOSTIC]',
|
||||||
|
'active tab diff event, active tab:',
|
||||||
|
diff.tabService.current,
|
||||||
|
);
|
||||||
|
const isDIAGNOSTICVisible = next.tabService.current === 'diagnostic';
|
||||||
|
|
||||||
|
if (isDIAGNOSTICVisible) {
|
||||||
|
logger.debug(
|
||||||
|
'[DIAGNOSTIC]',
|
||||||
|
'registerLifecycleListeners',
|
||||||
|
'onPageMount',
|
||||||
|
);
|
||||||
|
return onPageMount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDIAGNOSTICVisible) {
|
||||||
|
logger.debug(
|
||||||
|
'[DIAGNOSTIC]',
|
||||||
|
'registerLifecycleListeners',
|
||||||
|
'onPageUnmount',
|
||||||
|
);
|
||||||
|
return onPageUnmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initController(): Promise<void> {
|
||||||
|
onMount('diagnostic-status').then(() => {
|
||||||
|
logger.debug('[DIAGNOSTIC]', 'initController', 'onMount');
|
||||||
|
onPageMount();
|
||||||
|
registerLifecycleListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './renderAvailableActions';
|
||||||
|
export * from './renderCheckSection';
|
||||||
|
export * from './renderRunAction';
|
||||||
|
export * from './renderSystemInfo';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user