mirror of
https://github.com/itdoginfo/podkop.git
synced 2026-06-10 05:18:13 +03:00
Compare commits
419 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
847895598d | ||
|
|
4146804e5a | ||
|
|
daef80b7b8 | ||
|
|
6e7b8a6be2 | ||
|
|
1365febaa4 | ||
|
|
1543d974cc | ||
|
|
4b03c28c41 | ||
|
|
c3281773d5 | ||
|
|
c9e0b54f2c | ||
|
|
7f33cfcb98 | ||
|
|
177469e212 | ||
|
|
1802818044 | ||
|
|
c62cb2de14 | ||
|
|
f253e4e36f | ||
|
|
41e696c9b2 | ||
|
|
835b455915 | ||
|
|
6089ef4c9b | ||
|
|
617e9a93bc | ||
|
|
6d9aedd236 | ||
|
|
fc9cf79221 | ||
|
|
2184c0c7d4 | ||
|
|
03736c613b | ||
|
|
c37ea6a4d3 | ||
|
|
a0869da4a5 | ||
|
|
23ac3a8d12 | ||
|
|
d8bd87a7da | ||
|
|
b93403c3e3 | ||
|
|
5629425853 | ||
|
|
8b2abc0b28 | ||
|
|
21085f3ac3 | ||
|
|
9a015bbde4 | ||
|
|
a7b6c36dfb | ||
|
|
f39e2d9ca9 | ||
|
|
00594f23fd | ||
|
|
0379cf5966 | ||
|
|
cfee6cec9a | ||
|
|
ad760a78bf | ||
|
|
d854caa4cc | ||
|
|
b44274d7f2 | ||
|
|
12d8537d90 | ||
|
|
8e18426a53 | ||
|
|
41a4f67fd0 | ||
|
|
c30160d8d3 | ||
|
|
42f75c3374 | ||
|
|
2cce383b33 | ||
|
|
81963bfbdf | ||
|
|
f3d2fa5a52 | ||
|
|
474f4b197f | ||
|
|
6083009c80 | ||
|
|
0672c55188 | ||
|
|
e14a7e7dd1 | ||
|
|
3f7e7cbc4b | ||
|
|
782f08b71b | ||
|
|
a40240bb3f | ||
|
|
1e9a7bffa4 | ||
|
|
4448c09c34 | ||
|
|
af95c0dcd0 | ||
|
|
d2fbff17bf | ||
|
|
a73def1f9a | ||
|
|
9976065696 | ||
|
|
aa8f2cef41 | ||
|
|
96f6def701 | ||
|
|
0152f073b7 | ||
|
|
06ce944e1c | ||
|
|
64369a93b0 | ||
|
|
53a3c943f0 | ||
|
|
7c7e1c6244 | ||
|
|
7fc1f39dd6 | ||
|
|
1c4285dfa8 | ||
|
|
ea1273e05e | ||
|
|
5fc3c95928 | ||
|
|
dd3e70153a | ||
|
|
622e092317 | ||
|
|
c045f8f224 | ||
|
|
b45088dad7 | ||
|
|
82345047cb | ||
|
|
0a4ed367bc | ||
|
|
c3f322ae61 | ||
|
|
eb9239696e | ||
|
|
5b3421498e | ||
|
|
6a48a060e1 | ||
|
|
14f704fcb8 | ||
|
|
ff43f477e9 | ||
|
|
576e58fd17 | ||
|
|
d72c98a254 | ||
|
|
7a497f1e31 | ||
|
|
d52f6e26ae | ||
|
|
68c61aed50 | ||
|
|
626ac981eb | ||
|
|
352d10a047 | ||
|
|
031c419ffb | ||
|
|
c13fdf5785 | ||
|
|
1b7ab606ba | ||
|
|
2bf208ecac | ||
|
|
e256e4bee5 | ||
|
|
32c385b309 | ||
|
|
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 | ||
|
|
984ae5f2a9 | ||
|
|
7a62898541 | ||
|
|
7911d1d29f | ||
|
|
bc673b7881 | ||
|
|
0493565c5f | ||
|
|
4cd1094395 | ||
|
|
e87b431d86 | ||
|
|
b9ee917abf | ||
|
|
715a278af8 | ||
|
|
9bc2b5ffef | ||
|
|
9d89258c0c | ||
|
|
52d1c5d95f | ||
|
|
587e5245d3 | ||
|
|
e7578d61bc | ||
|
|
9918b71a82 | ||
|
|
f48c4ff2bb | ||
|
|
e77bcc386a | ||
|
|
455c19ab2e | ||
|
|
914e1792f3 | ||
|
|
826245a89a | ||
|
|
b5cfc017fe | ||
|
|
267fd2b793 | ||
|
|
c0b400dfb0 | ||
|
|
752636347e | ||
|
|
28aeb29c51 | ||
|
|
6ff543d7fb | ||
|
|
b89fe33296 | ||
|
|
3d63a82815 | ||
|
|
934f802879 | ||
|
|
4d0755e4c0 | ||
|
|
88ee7b4a54 | ||
|
|
0eb575d171 | ||
|
|
9a46d731c9 | ||
|
|
a45ab62885 | ||
|
|
b7bad57299 | ||
|
|
4ac755bd36 | ||
|
|
e9a0c96882 | ||
|
|
48c8f01d2f | ||
|
|
72b2a34af9 | ||
|
|
ae4a3781e6 | ||
|
|
1bce7c0c98 | ||
|
|
a8b2001cc1 | ||
|
|
d6481675e0 | ||
|
|
2ba1c2f740 | ||
|
|
5d0f8ce5bf | ||
|
|
ddad137fc1 | ||
|
|
7b2e5d2838 | ||
|
|
9a72785fa7 | ||
|
|
e0874c3775 | ||
|
|
1e6c827f2b | ||
|
|
c8c0025470 | ||
|
|
c78f97d64f | ||
|
|
7cb43ffb65 | ||
|
|
1e4cda9400 | ||
|
|
caf82b096f | ||
|
|
6117b0ef9b | ||
|
|
5418187dd3 | ||
|
|
31b09cc3d2 | ||
|
|
b2a473573b | ||
|
|
aad6d8c002 | ||
|
|
c75dd3e78b | ||
|
|
341f260fcf | ||
|
|
c5e19a0f2d | ||
|
|
d50b6dbab6 | ||
|
|
99c8ead148 | ||
|
|
d605094a9d | ||
|
|
eb60e6edec | ||
|
|
08f5b31d58 | ||
|
|
f69e3478c8 | ||
|
|
d9a4f50f62 | ||
|
|
eb52d52eb4 | ||
|
|
3f4a0cf094 | ||
|
|
b0a8526c90 | ||
|
|
e9d5b18816 | ||
|
|
7b06f422af | ||
|
|
96bcc36cf1 | ||
|
|
db8e8e8298 | ||
|
|
eb0617eef1 | ||
|
|
8f9bff9a64 | ||
|
|
65d3a9253f | ||
|
|
b99116fbf3 | ||
|
|
8f19f31e7a | ||
|
|
327c3d2b68 | ||
|
|
260b7b9558 | ||
|
|
df9dba9742 | ||
|
|
547feb0e06 | ||
|
|
77e141b305 | ||
|
|
cfc5d995a8 | ||
|
|
e84233a10c | ||
|
|
b71c7b379d | ||
|
|
3988588c9f | ||
|
|
cd133838cb | ||
|
|
f58472a53d | ||
|
|
5e95148492 | ||
|
|
df9400514b | ||
|
|
14eec8e600 | ||
|
|
294cb21e91 | ||
|
|
4ef15f7340 | ||
|
|
41563a5828 | ||
|
|
2e99ee3a17 | ||
|
|
a8db33dd28 | ||
|
|
1295e0dcb2 | ||
|
|
b6bec0fc51 | ||
|
|
769d263be2 | ||
|
|
470f11699c | ||
|
|
852b6c043a | ||
|
|
f5cafd5573 | ||
|
|
3562b913a2 | ||
|
|
f4ac9dcc77 | ||
|
|
f5a629afcf | ||
|
|
aea201bf24 | ||
|
|
1313c3b26f | ||
|
|
a3f4e942c3 | ||
|
|
4d8e4c1c13 | ||
|
|
0cb5c2daae | ||
|
|
19fbfff555 | ||
|
|
75a2ed1e29 | ||
|
|
759b6748c6 | ||
|
|
0a27784f85 | ||
|
|
3b95ac2bc3 | ||
|
|
5c51d99d73 | ||
|
|
904b90e012 | ||
|
|
5fb8343cf8 | ||
|
|
014f0f4bdf | ||
|
|
dd44e0156e | ||
|
|
927b8a53b0 | ||
|
|
7ba20905d5 | ||
|
|
5b15a56502 | ||
|
|
c31df68bec | ||
|
|
0a5229f4f6 | ||
|
|
5ecb6ef997 | ||
|
|
340c2b3505 | ||
|
|
515c0be38b | ||
|
|
59c59bcb17 | ||
|
|
e5eff41a0f | ||
|
|
bb1c06951c | ||
|
|
4999840340 | ||
|
|
6c5a271105 | ||
|
|
e336bb831c | ||
|
|
00db99723c | ||
|
|
5439504de7 | ||
|
|
c3072162de | ||
|
|
d021636f85 | ||
|
|
a06aac0613 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
123
.github/workflows/build.yml
vendored
123
.github/workflows/build.yml
vendored
@@ -2,53 +2,118 @@ name: Build packages
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build podkop and luci-app-podkop
|
||||
preparation:
|
||||
name: Setup build version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.1
|
||||
- uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: version
|
||||
run: |
|
||||
VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "0.$(date +%d%m%Y)")
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Builder for ${{ matrix.package_type }} podkop and luci-app-podkop
|
||||
runs-on: ubuntu-latest
|
||||
needs: preparation
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { package_type: ipk }
|
||||
- { package_type: apk }
|
||||
steps:
|
||||
- uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "dev_$(date +%d%m%Y)")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
- name: Build ${{ matrix.package_type }}
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
file: ./Dockerfile-${{ matrix.package_type }}
|
||||
context: .
|
||||
tags: podkop:ci
|
||||
tags: podkop:ci-${{ matrix.package_type }}
|
||||
build-args: |
|
||||
PKG_VERSION=${{ steps.version.outputs.version }}
|
||||
PODKOP_VERSION=${{ needs.preparation.outputs.version }}
|
||||
|
||||
- name: Create Docker container
|
||||
run: docker create --name podkop podkop:ci
|
||||
- name: Create ${{ matrix.package_type }} Docker container
|
||||
run: docker create --name ${{ matrix.package_type }} podkop:ci-${{ matrix.package_type }}
|
||||
|
||||
- name: Copy file from Docker container
|
||||
- name: Copy files from ${{ matrix.package_type }} Docker container
|
||||
run: |
|
||||
docker cp podkop:/builder/bin/packages/x86_64/utilites/. ./bin/
|
||||
docker cp podkop:/builder/bin/packages/x86_64/luci/. ./bin/
|
||||
mkdir -p ./bin/${{ matrix.package_type }}
|
||||
docker cp ${{ matrix.package_type }}:/builder/bin/packages/x86_64/utilities/. ./bin/${{ matrix.package_type }}/
|
||||
docker cp ${{ matrix.package_type }}:/builder/bin/packages/x86_64/luci/. ./bin/${{ matrix.package_type }}/
|
||||
|
||||
- name: Filter IPK files
|
||||
# IPK uses underscore `_` in filenames, while APK uses only dash `-`
|
||||
- name: Fix naming difference between build for packages (replace _ with -)
|
||||
if: matrix.package_type == 'ipk'
|
||||
shell: bash
|
||||
run: |
|
||||
# Извлекаем версию из тега, убирая префикс 'v'
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
for f in ./bin/${{ matrix.package_type }}/*.${{ matrix.package_type }}; do
|
||||
[ -e "$f" ] || continue
|
||||
base=$(basename "$f")
|
||||
newname=$(echo "$base" | sed 's/_/-/g')
|
||||
mv "$f" "./bin/${{ matrix.package_type }}/$newname"
|
||||
done
|
||||
|
||||
mkdir -p ./filtered-bin
|
||||
cp ./bin/luci-i18n-podkop-ru_*.ipk "./filtered-bin/luci-i18n-podkop-ru_${VERSION}.ipk"
|
||||
cp ./bin/podkop_*.ipk ./filtered-bin/
|
||||
cp ./bin/luci-app-podkop_*.ipk ./filtered-bin/
|
||||
- name: Filter files
|
||||
shell: bash
|
||||
run: |
|
||||
# Use version from preparation job (already without 'v' prefix)
|
||||
VERSION="${{ needs.preparation.outputs.version }}"
|
||||
|
||||
mkdir -p ./filtered-bin/${{ matrix.package_type }}
|
||||
cp ./bin/${{ matrix.package_type }}/luci-i18n-podkop-ru-*.${{ matrix.package_type }} "./filtered-bin/${{ matrix.package_type }}/luci-i18n-podkop-ru-${VERSION}.${{ matrix.package_type }}"
|
||||
cp ./bin/${{ matrix.package_type }}/podkop-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/
|
||||
cp ./bin/${{ matrix.package_type }}/luci-app-podkop-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/
|
||||
|
||||
- name: Remove Docker container
|
||||
run: docker rm podkop
|
||||
run: docker rm ${{ matrix.package_type }}
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: release-files-${{ github.ref_name }}-${{ matrix.package_type }}
|
||||
path: ./filtered-bin/${{ matrix.package_type }}/*.${{ matrix.package_type }}
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v5.0.0
|
||||
- name: Create release dir
|
||||
run: mkdir -p ./filtered-bin/release
|
||||
|
||||
- name: Download ipk artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-files-${{ github.ref_name }}-ipk
|
||||
path: ./filtered-bin/release
|
||||
|
||||
- name: Download apk artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-files-${{ github.ref_name }}-apk
|
||||
path: ./filtered-bin/release
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
uses: softprops/action-gh-release@v2.4.0
|
||||
with:
|
||||
files: ./filtered-bin/*.ipk
|
||||
files: ./filtered-bin/release/*.*
|
||||
draft: false
|
||||
prerelease: false
|
||||
name: ${{ github.ref_name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
29
.github/workflows/close-invalid-issues.yml
vendored
Normal file
29
.github/workflows/close-invalid-issues.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Close Invalid Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
close-invalid:
|
||||
if: github.event.label.name == 'Close as invalid'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Close issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: 'This issue has been marked as invalid and is being closed automatically.'
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
78
.github/workflows/frontend-ci.yml
vendored
Normal file
78
.github/workflows/frontend-ci.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Frontend CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'fe-app-podkop/**'
|
||||
- '.github/workflows/frontend-ci.yml'
|
||||
|
||||
jobs:
|
||||
frontend-checks:
|
||||
name: Frontend Quality Checks
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: fe-app-podkop
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
working-directory: fe-app-podkop
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache yarn dependencies
|
||||
uses: actions/cache@v4.3.0
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('fe-app-podkop/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Check formatting
|
||||
id: format
|
||||
run: |
|
||||
yarn format
|
||||
if ! git diff --exit-code; then
|
||||
echo "::error::Code is not formatted. Run 'yarn format' locally."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run linter
|
||||
run: yarn lint --max-warnings=0
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test --run
|
||||
|
||||
- name: Build project
|
||||
id: build
|
||||
run: |
|
||||
yarn build
|
||||
if ! git diff --exit-code; then
|
||||
echo "::error::Build generated changes. Check build output."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Frontend CI Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Format check: ${{ steps.format.outcome }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Lint check: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Tests: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Build: ${{ steps.build.outcome }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
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 }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
.idea
|
||||
.idea
|
||||
fe-app-podkop/node_modules
|
||||
fe-app-podkop/.env
|
||||
.DS_Store
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
FROM itdoginfo/openwrt-sdk:24.10.1
|
||||
|
||||
ARG PKG_VERSION
|
||||
ENV PKG_VERSION=${PKG_VERSION}
|
||||
|
||||
COPY ./podkop /builder/package/feeds/utilites/podkop
|
||||
COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop
|
||||
|
||||
RUN make defconfig && make package/podkop/compile && make package/luci-app-podkop/compile V=s -j4
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM openwrt/sdk:x86_64-v24.10.1
|
||||
|
||||
RUN ./scripts/feeds update -a && ./scripts/feeds install luci-base && mkdir -p /builder/package/feeds/utilites/ && mkdir -p /builder/package/feeds/luci/
|
||||
11
Dockerfile-apk
Normal file
11
Dockerfile-apk
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM itdoginfo/openwrt-sdk-apk:25.12.3
|
||||
|
||||
ARG PODKOP_VERSION
|
||||
ENV PODKOP_VERSION=${PODKOP_VERSION}
|
||||
|
||||
COPY ./podkop /builder/package/feeds/utilities/podkop
|
||||
COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop
|
||||
|
||||
RUN make defconfig && \
|
||||
make package/podkop/compile -j4 V=s && \
|
||||
make package/luci-app-podkop/compile -j4 V=s
|
||||
11
Dockerfile-ipk
Normal file
11
Dockerfile-ipk
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM itdoginfo/openwrt-sdk-ipk:24.10.6
|
||||
|
||||
ARG PODKOP_VERSION
|
||||
|
||||
COPY ./podkop /builder/package/feeds/utilities/podkop
|
||||
COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop
|
||||
|
||||
RUN export PODKOP_VERSION="v${PODKOP_VERSION}" && \
|
||||
make defconfig && \
|
||||
make package/podkop/compile V=s -j4 && \
|
||||
make package/luci-app-podkop/compile V=s -j4
|
||||
79
README.md
79
README.md
@@ -1,55 +1,54 @@
|
||||
# Вещи, которые вам нужно знать перед установкой
|
||||
# Podkop
|
||||
|
||||
Маршрутизация трафика для OpenWrt.
|
||||
|
||||
Направляйте нужные ресурсы в туннель, а остальное — напрямую. Открытое программное обеспечение на базе [sing-box](https://github.com/SagerNet/sing-box).
|
||||
|
||||
> [!WARNING]
|
||||
> Проект находится в стадии бета-версии. Возможны ошибки, нестабильная работа и существенные изменения функциональности.
|
||||
|
||||
# Вещи, которые необходимо знать перед установкой
|
||||
|
||||
### Обновления и конфигурация
|
||||
- При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
|
||||
- После обновления проверяйте конфигурацию — она может изменяться между версиями.
|
||||
- При старте Podkop модифицируется конфигурация Dnsmasq.
|
||||
- Podkop изменяет конфигурацию sing-box. Если вы используете собственную конфигурацию, заранее сохраните её.
|
||||
|
||||
### Системные требования
|
||||
- Требуется OpenWrt 24.10 или выше.
|
||||
- Необходимо минимум 25 МБ свободного места на устройстве. Устройства с флеш-памятью 16 МБ не поддерживаются.
|
||||
|
||||
### Важные ограничения и особенности
|
||||
- Если установлен Getdomains, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления)
|
||||
- Dashboard доступен только при подключении по HTTP (из-за особенностей Clash API). При использовании HTTPS или домена работа может быть недоступна.
|
||||
|
||||
### Поддержка и диагностика
|
||||
- [Руководство по диагностике](https://podkop.net/docs/diagnostics/)
|
||||
- Актуальные изменения публикуются в [Telegram-чате](https://t.me/itdogchat/81758/420321). Пожалуйста, ознакомьтесь с закрепленными сообщениями.
|
||||
- При возникновении проблем оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате.
|
||||
|
||||
- Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
|
||||
- При возникновении проблем, нужен технически грамотный фидбэк в чат.
|
||||
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
|
||||
- Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
|
||||
- Необходимо минимум 25МБ свободного места на роутере. Роутеры с флешками на 16МБ сразу мимо.
|
||||
- При старте программы редактируется конфиг Dnsmasq.
|
||||
- Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен.
|
||||
- Информация здесь может быть устаревшей. Все изменения фиксируются в [телеграм-чате](https://t.me/itdogchat/81758/420321).
|
||||
- [Если у вас не что-то не работает.](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).
|
||||
- Требуется версия OpenWrt 24.10.
|
||||
|
||||
# Документация
|
||||
https://podkop.net/
|
||||
|
||||
# Установка Podkop
|
||||
Полная информация в [документации](https://podkop.net/docs/install/)
|
||||
Полное руководство доступно в [документации](https://podkop.net/docs/install/)
|
||||
|
||||
Вкратце, достаточно одного скрипта для установки и обновления:
|
||||
Для установки и обновления достаточно выполнить один скрипт:
|
||||
```
|
||||
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
|
||||
```
|
||||
|
||||
# ToDo
|
||||
Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме.
|
||||
|
||||
Основные задачи в issues.
|
||||
|
||||
## Рефактор
|
||||
- [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 из фильтров.
|
||||
- [x] Опция, когда все запросы (с роутера в первую очередь), а не только br-lan идут в прокси. С этим связана #95. Требуется много переделать для nftables.
|
||||
- [ ] Весь трафик в Proxy\VPN. Вопрос, что делать с экстрасекциями в этом случае. FakeIP здесь скорее не нужен, а значит только main секция остаётся. Всё что касается fakeip проверок, придётся выключать в этом режиме.
|
||||
- [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 серверам.
|
||||
- [ ] IPv6. Только после наполнения Wiki.
|
||||
Планы развития проекта в настоящее время не публикуются в виде открытого roadmap. Обсуждение направлений и задач разработки ведётся авторами и контрибьюторами.
|
||||
|
||||
## Тесты
|
||||
- [ ] Unit тесты (BATS)
|
||||
- [ ] Интеграционые тесты бекенда (OpenWrt rootfs + BATS)
|
||||
> [!IMPORTANT]
|
||||
> Pull Request принимаются только после согласования с авторами в Telegram-чате. На данный момент PR без предварительного обсуждения не рассматриваются.
|
||||
|
||||
> [!WARNING]
|
||||
> Данное программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели.
|
||||
>
|
||||
> Правообладатели и участники проекта не несут ответственности за любые прямые, косвенные, случайные, специальные или иные убытки, возникшие в результате использования программного обеспечения, включая потерю данных, прибыли или прерывание деятельности, даже если они были предупреждены о возможности таких последствий.
|
||||
|
||||
[](https://deepwiki.com/itdoginfo/podkop)
|
||||
@@ -1,68 +1,119 @@
|
||||
# Shadowsocks
|
||||
Тут всё просто
|
||||
|
||||
## Shadowsocks-old
|
||||
## Socks
|
||||
```
|
||||
ss://YWVzLTI1Ni1nY206RmJwUDJnSStPczJKK1kzdkVhTnVuOUZ2ZjJZYUhNUlN1L1BBdEVqMks1VT0@example.com:80?type=tcp#example-ss-old
|
||||
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-2022
|
||||
## Shadowsocks
|
||||
```
|
||||
ss://2022-blake3-aes-128-gcm:5NgF%2B9eM8h4OnrTbHp%2B8UA%3D%3D%3Am8tbs5aKLYG7dN9f3xsiKA%3D%3D@example.com:80#example-ss2022
|
||||
ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client
|
||||
ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206S3FiWXZiNkhwb1RmTUt0N2VGcUZQSmJNNXBXaHlFU0ZKTXY2dEp1Ym1Fdz06dzRNMEx5RU9OTGQ5SWlkSGc0endTbzN2R3h4NS9aQ3hId0FpaWlxck5hcz0@127.0.0.1:26627?type=tcp#shadowsocks-client
|
||||
ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@127.0.0.1:27214?type=tcp#shadowsocks-plain-user
|
||||
```
|
||||
|
||||
## VLESS
|
||||
```
|
||||
ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Y21lZklCdDhwMTJaZm1QWUplMnNCNThRd3R3NXNKeVpUV0Z6ZENKV2taOD06eEJHZUxiMWNPTjFIeE9CenF6UlN0VFdhUUh6YWM2cFhRVFNZd2dVV2R1RT0@example.com:81?type=tcp#example-ss2022
|
||||
```
|
||||
Может быть без `?type=tcp`
|
||||
# tcp
|
||||
vless://94792286-7bbe-4f33-8b36-18d1bbf70723@127.0.0.1:34520?type=tcp&encryption=none&security=none#vless-tcp-none
|
||||
vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni=google.com&sid=f6&spx=%2F#vless-tcp-reality
|
||||
vless://2e9e8288-060e-4da2-8b9f-a1c81826feb7@127.0.0.1:19316?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-tcp-tls
|
||||
vless://0235c833-dc29-4202-8a7b-1bbba5b516a2@127.0.0.1:22993?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#vless-tcp-tls-insecure
|
||||
vless://17776137-e747-4268-a84d-99fd798accac@127.0.0.1:48076?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com&ech=AFP%2BDQBPAAAgACDJXiKG5eoCHfd1MbMxgccxgrbGisBPPe3bz1KVIETUXQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAAAAAA%3D%3D#vless-tcp-tls-ech
|
||||
|
||||
# VLESS
|
||||
# mKCP
|
||||
vless://72e201d7-7841-4a32-b266-4aa3eb776d51@127.0.0.1:17270?type=kcp&encryption=none&headerType=none&seed=AirziWi4ng&security=none#vless-mKCP
|
||||
|
||||
## Reality
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=reality&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&fp=chrome&sni=sni.server.com&sid=6cabf01472a3&spx=%2F&flow=xtls-rprx-vision#vless-reality
|
||||
# WebSocket
|
||||
vless://d86daef7-565b-4ecd-a9ee-bac847ad38e6@127.0.0.1:12928?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=none#vless-websocket-none
|
||||
vless://fe0f0941-09a9-4e46-bc69-e00190d7bb9c@127.0.0.1:10156?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-websocket-tls
|
||||
vless://599e8659-e2ef-47d9-bf72-2f9b4b673474@127.0.0.1:36567?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#vless-websocket-tls-insecure
|
||||
vless://4d21ce62-8723-4c4d-93e3-d586b107aa40@127.0.0.1:51394?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com&ech=AF3%2BDQBZAAAgACD7fjrtDMlcigKXFBKoLn6UDB9%2BWR6HBZpY96DlBiD%2BIwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D#vless-websocket-tls-ech
|
||||
|
||||
# gRPC
|
||||
vless://974b39e3-f7bf-42b9-933c-16699c635e77@127.0.0.1:15633?type=grpc&encryption=none&serviceName=TunService&authority=&security=none#vless-gRPC-none
|
||||
vless://651e7eca-5152-46f1-baf2-d502e0af7b27@127.0.0.1:28535?type=grpc&encryption=none&serviceName=TunService&authority=authority&security=reality&pbk=nhZ7NiKfcqESa5ZeBFfsq9o18W-OWOAHLln9UmuVXSk&fp=chrome&sni=google.com&sid=11cbaeaa&spx=%2F#vless-gRPC-reality
|
||||
vless://221ff905-b783-41a0-a6a6-8089eaf3b34b@abc.def.xyz:443?security=reality&type=grpc&headerType=&authority=abc.def.xyz&serviceName=name&mode=gun&sni=abc.def.xyz&fp=chrome&pbk=C3nhDJw02ZU_rjx4GbC54Sp79-ysF5lWIQVWdY4FOnE&sid=#vless-gRPC-reality-mode
|
||||
vless://af1f8b5f-26c9-4fe8-8ce7-6d6366c5c9ce@127.0.0.1:47904?type=grpc&encryption=none&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-gRPC-tls
|
||||
vless://95f2c4bb-abcb-47ba-bfad-e181c03e4659@127.0.0.1:34530?type=grpc&encryption=none&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#vless-gRPC-tls-insecure
|
||||
vless://bd39490f-9a4f-49b2-96b6-824190cf89e9@127.0.0.1:27779?type=grpc&encryption=none&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com&ech=AF3%2BDQBZAAAgACBc%2FiNdo4QkTt9eQCQgkOiJVSfA9G6UWAyipaBFtBD%2FVQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D#vless-gRPC-tls-ech
|
||||
|
||||
# HTTPUpgrade
|
||||
vless://2b98f144-847f-42f7-8798-e1a32d27bdc7@127.0.0.1:47154?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=none#vless-httpupgrade-none
|
||||
vless://76dbd0ff-1a35-4f0c-a9ba-3c5890b7dea6@127.0.0.1:50639?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-httpupgrade-tls
|
||||
vless://6d229881-50ed-4f3f-995d-bd3e725fdbff@127.0.0.1:57616?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#vless-httpupgrade-tls-insecure
|
||||
vless://1897e9e4-6f5d-4a85-9512-9192e76c3f04@127.0.0.1:38658?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com&ech=AF3%2BDQBZAAAgACCmXTMzlrdcCk2FyINAWKZ4DBxq4%2BCgmJ69v%2BmH4EMlEQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D#vless-httpupgrade-tls-ech
|
||||
|
||||
# XHTTP
|
||||
vless://c2841505-ec32-4b8d-b6dd-3e19d648c321@127.0.0.1:45507?type=xhttp&encryption=none&path=%2Fxhttppath&host=xhttp&mode=auto&security=none#vless-xhttp
|
||||
```
|
||||
|
||||
## Trojan
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@123.123.123.123:2082?security=reality&sni=sni.server.com&alpn=h2,http/1.1&allowInsecure=1&fp=chrome&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&sid=6cabf01472a3&type=grpc&encryption=none#vless-reality-strange
|
||||
# tcp
|
||||
trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none
|
||||
trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni=google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality
|
||||
trojan://EJjpAj02lg@127.0.0.1:11381?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-tcp-tls
|
||||
trojan://ZP2Ik5sxN3@127.0.0.1:16247?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-tcp-tls-insecure
|
||||
trojan://90caP481ay@127.0.0.1:59708?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACC2y%2BAe4dqthLNpfvmtE6g%2BnaJ%2FciK6P%2BREbRLkR%2Fg%2FEgAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-tcp-tls-ech
|
||||
|
||||
# mKCP
|
||||
trojan://N5v7iIOe9G@127.0.0.1:36319?type=kcp&headerType=none&seed=P91wFIfjzZ&security=none#trojan-mKCP
|
||||
|
||||
# WebSocket
|
||||
trojan://G3cE9phv1g@127.0.0.1:57370?type=ws&path=%2Fwspath&host=google.com&security=none#trojan-websocket-none
|
||||
trojan://FBok41WczO@127.0.0.1:59919?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-websocket-tls
|
||||
trojan://bhwvndUBPA@127.0.0.1:22969?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-websocket-tls-insecur
|
||||
trojan://pwiduqFUWO@127.0.0.1:46765?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCFcQYEtwrFOidJJLYHvSiN%2BljRgaAIrNHoVnio3uXAOwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-websocket-tls-ech
|
||||
|
||||
# gRPC
|
||||
trojan://WMR7qkKhsV@127.0.0.1:27897?type=grpc&serviceName=TunService&authority=authority&security=none#trojan-gRPC-none
|
||||
trojan://KVuRNsu6KG@127.0.0.1:46077?type=grpc&serviceName=TunService&authority=authority&security=reality&pbk=Xn59i4gum3ppCICS6-_NuywrhHIVVAH54b2mjd5CFkE&fp=chrome&sni=google.com&sid=e5be&spx=%2F#trojan-gRPC-reality
|
||||
trojan://7BJtbywy8h@127.0.0.1:10627?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-gRPC-tls
|
||||
trojan://TI3PakvtP4@127.0.0.1:10435?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-gRPC-tls-insecure
|
||||
trojan://mbzoVKL27h@127.0.0.1:38681?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCq72Ru3VbFlDpKttl3LccmInu8R2oAsCr8wzyxB0vZZQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-gRPC-tls-ech
|
||||
|
||||
# HTTPUpgrade
|
||||
trojan://uc44gBwOKQ@127.0.0.1:29085?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=none#trojan-httpupgrade-none
|
||||
trojan://MhNxbcVB14@127.0.0.1:32700?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-httpupgrade-tls
|
||||
trojan://7SOQFUpLob@127.0.0.1:28474?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-httpupgrade-tls-insecure
|
||||
trojan://ou8pLSyx9N@127.0.0.1:17737?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACB%2FlkIkit%2BblFzE7PtbYDVF3NXK8olXJ5a7YwY%2Biy9QQwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-httpupgrade-tls-ech
|
||||
|
||||
# XHTTP
|
||||
trojan://VEetltxLtw@127.0.0.1:59072?type=xhttp&path=%2Fxhttppath&host=google.com&mode=auto&security=none#trojan-xhttp
|
||||
```
|
||||
|
||||
## TLS
|
||||
1.
|
||||
## Hysteria2
|
||||
|
||||
hysteria2://
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=tls&fp=&alpn=h3%2Ch2%2Chttp%2F1.1#vless-tls
|
||||
# With password
|
||||
hysteria2://password@example.com:443/#hysteria2-password
|
||||
hysteria2://password@example.com:443/?insecure=1#hysteria2-password-insecure
|
||||
|
||||
# With SNI
|
||||
hysteria2://password@example.com:443/?sni=example.com#hysteria2-password-sni
|
||||
|
||||
# With obfuscation
|
||||
hysteria2://password@example.com:443/?obfs=salamander&obfs-password=obfspassword#hysteria2-obfs
|
||||
|
||||
# All parameters combined
|
||||
hysteria2://mypassword@example.com:8443/?sni=example.com&obfs=salamander&obfs-password=obfspass&insecure=1#hysteria2-all-params
|
||||
```
|
||||
|
||||
2.
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&fp=chrome&type=tcp&flow=xtls-rprx-vision&encryption=none#vless-tls-withot-alpn
|
||||
```
|
||||
3.
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws
|
||||
hy2://
|
||||
```
|
||||
# With password
|
||||
hy2://password@example.com:443/#hysteria2-password
|
||||
hy2://password@example.com:443/?insecure=1#hysteria2-password-insecure
|
||||
|
||||
4.
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&type=ws&path=/?ed%3D2560&host=sni.server.com&encryption=none#vless-tls-ws-2
|
||||
```
|
||||
# With SNI
|
||||
hy2://password@example.com:443/?sni=example.com#hysteria2-password-sni
|
||||
|
||||
5.
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&fp=chrome&type=ws&path=/websocket&encryption=none#vless-tls-ws-3
|
||||
```
|
||||
# With obfuscation
|
||||
hy2://password@example.com:443/?obfs=salamander&obfs-password=obfspassword#hysteria2-obfs
|
||||
|
||||
6.
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws-4
|
||||
```
|
||||
|
||||
7.
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@sub.example.com:443?type=ws&path=%2Fdir%2Fpath&host=sub.example.com&security=tls#configname
|
||||
```
|
||||
|
||||
## No security
|
||||
```
|
||||
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=none#vless-tls-no-encrypt
|
||||
# All parameters combined
|
||||
hy2://mypassword@example.com:8443/?sni=example.com&obfs=salamander&obfs-password=obfspass&insecure=1#hysteria2-all-params
|
||||
```
|
||||
63
TRADEMARK.md
Normal file
63
TRADEMARK.md
Normal file
@@ -0,0 +1,63 @@
|
||||
Trademark Guidelines
|
||||
|
||||
Version 1.0 dated May 28, 2026
|
||||
|
||||
This trademark policy was prepared to help you understand how to use the Podkop trademarks, service marks, and logos in connection with the Podkop open source project and related software.
|
||||
|
||||
While the Podkop software is available under an open source license, that license does not grant permission to use the Podkop trademarks, service marks, or logos. This policy explains acceptable use of the Podkop brand and related marks.
|
||||
|
||||
This Policy covers:
|
||||
1. Our word trademarks and service marks: Podkop
|
||||
2. Our logos, icons, and other Podkop brand assets
|
||||
|
||||
This policy encompasses all trademarks and service marks, whether they are registered or not.
|
||||
|
||||
## 1. General Guidelines
|
||||
|
||||
Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom. For example, you cannot say you are distributing Podkop software when you are distributing a modified version of it, because recipients may not understand the differences between your modified versions and our own.
|
||||
|
||||
You also cannot use our logo on your website in a way that suggests that your website is an official website or that we endorse your website.
|
||||
|
||||
You can, however, say that you like the Podkop project, that you participate in the Podkop community, or that you are providing an unmodified version of the Podkop software.
|
||||
|
||||
You may not use or register our marks, or variations of them as part of your own trademark, service mark, domain name, company name, trade name, product name or service name.
|
||||
|
||||
Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service. For example, we would consider the following too similar to one of our Marks:
|
||||
|
||||
- MyPodkop
|
||||
- Open-Podkop
|
||||
- PodkopX
|
||||
- Podkop Lite
|
||||
- Podkop Pro
|
||||
|
||||
## 2. Acceptable Uses
|
||||
### Unmodified Code
|
||||
|
||||
When you redistribute an unmodified copy of Podkop software, you must not remove any Podkop trademarks, notices, or branding included in the original distribution.
|
||||
|
||||
### Modified Code
|
||||
|
||||
If you distribute a modified version of Podkop software, you may not use the Podkop name, trademarks, or logos in connection with your modified version, except to accurately describe the origin of the software in factual statements.
|
||||
|
||||
You must replace any Podkop branding, including names displayed in user interfaces, logs, documentation, and other user-facing elements, with your own distinct name and branding, so that your modified version is clearly distinguishable from the original Podkop software.
|
||||
|
||||
You must remove all Podkop logos and any other brand assets from the modified version.
|
||||
|
||||
You may not present your modified version as Podkop or as an official Podkop release, nor may you use the Podkop name in a way that suggests endorsement, affiliation, or official status.
|
||||
|
||||
You may only refer to Podkop in a factual and descriptive manner, for example: “This software is derived from Podkop open-source software.”
|
||||
|
||||
### Statements about Compatibility
|
||||
|
||||
You may use the word marks, exclusively for descriptive and factual purposes, to truthfully and accurately describe the relationship between your software and ours, including compatibility or origin. Any other use may imply that we have certified or approved your software.
|
||||
|
||||
### No Domain Names
|
||||
|
||||
You must not register any domain that includes our word marks or any variant or combination of them.
|
||||
|
||||
## 3. How to Display our Marks
|
||||
### Use of Trademarks in Text
|
||||
|
||||
Always use trademarks in their exact form with correct spelling. They must not be abbreviated, modified, hyphenated, or combined with other words in a way that creates a new product or service name.
|
||||
|
||||
Unacceptable: Podcop
|
||||
65
TRADEMARK_RU.md
Normal file
65
TRADEMARK_RU.md
Normal file
@@ -0,0 +1,65 @@
|
||||
Руководство по использованию товарных знаков
|
||||
|
||||
Версия 1.0 от 28 мая 2026 года
|
||||
|
||||
Настоящая политика в отношении товарных знаков подготовлена для того, чтобы помочь вам понять, как использовать товарные знаки, знаки обслуживания и логотипы Podkop в связи с открытым исходным кодом проекта Podkop и связанным программным обеспечением.
|
||||
|
||||
Хотя программное обеспечение Podkop распространяется под лицензией с открытым исходным кодом, эта лицензия не предоставляет разрешения на использование товарных знаков Podkop, знаков обслуживания или логотипов. Данная политика объясняет допустимое использование бренда Podkop и связанных обозначений.
|
||||
|
||||
Настоящая Политика охватывает:
|
||||
1. Наши словесные товарные знаки и знаки обслуживания: Podkop
|
||||
2. Наши логотипы, иконки и другие бренд-активы Podkop
|
||||
|
||||
Данная политика распространяется на все товарные знаки и знаки обслуживания, независимо от того, зарегистрированы они или нет.
|
||||
|
||||
## 1. Общие рекомендации
|
||||
|
||||
При использовании любого из наших знаков вы всегда должны делать это таким образом, чтобы никого не вводить в заблуждение относительно того, что именно они получают и от кого. Например, вы не можете утверждать, что распространяете программное обеспечение Podkop, если вы распространяете его модифицированную версию, поскольку получатели могут не понимать различий между вашей модифицированной версией и нашей оригинальной.
|
||||
|
||||
Вы также не можете использовать наш логотип на своём сайте таким образом, чтобы это создавало впечатление, что ваш сайт является официальным сайтом или что мы одобряем ваш сайт.
|
||||
|
||||
Однако вы можете указывать, что вам нравится проект Podkop, что вы участвуете в сообществе Podkop или что вы распространяете немодифицированную версию программного обеспечения Podkop.
|
||||
|
||||
Вы не имеете права использовать или регистрировать наши знаки, а также их вариации, как часть вашего собственного товарного знака, знака обслуживания, доменного имени, названия компании, коммерческого наименования, названия продукта или услуги.
|
||||
|
||||
Закон о товарных знаках не допускает использование названий или знаков, которые слишком похожи на наши. Поэтому вы не можете использовать очевидные вариации наших знаков или любые фонетически, иностранно-языковые эквиваленты, производные, аббревиатуры для похожего или совместимого продукта или услуги. Например, мы считаем слишком похожими на наши знаки следующие варианты:
|
||||
|
||||
- MyPodkop
|
||||
- Open-Podkop
|
||||
- PodkopX
|
||||
- Podkop Lite
|
||||
- Podkop Pro
|
||||
|
||||
## 2. Допустимое использование
|
||||
|
||||
### Немодифицированный код
|
||||
|
||||
При распространении немодифицированной копии программного обеспечения Podkop вы не должны удалять товарные знаки, уведомления или брендинг Podkop, включённые в исходное распространение.
|
||||
|
||||
### Модифицированный код
|
||||
|
||||
Если вы распространяете модифицированную версию программного обеспечения Podkop, вы не можете использовать название Podkop, товарные знаки или логотипы в связи с вашей модифицированной версией, за исключением точного описания происхождения программного обеспечения в фактических утверждениях.
|
||||
|
||||
Вы обязаны заменить все элементы брендинга Podkop, включая названия, отображаемые в пользовательском интерфейсе, логах, документации и других пользовательских элементах, на собственное отличительное название и брендинг, чтобы ваша модифицированная версия была явно отличима от оригинального программного обеспечения Podkop.
|
||||
|
||||
Вы должны удалить все логотипы Podkop и любые другие бренд-материалы из модифицированной версии.
|
||||
|
||||
Вы не можете представлять вашу модифицированную версию как Podkop или как официальную версию Podkop, а также использовать название Podkop таким образом, чтобы это подразумевало одобрение, аффилированность или официальный статус.
|
||||
|
||||
Вы можете ссылаться на Podkop только в фактическом и описательном контексте, например: «Это программное обеспечение основано на программном обеспечении Podkop с открытым исходным кодом».
|
||||
|
||||
### Упоминания о совместимости
|
||||
|
||||
Вы можете использовать словесные знаки исключительно в описательных и фактических целях для правдивого и точного описания связи вашего программного обеспечения с нашим, включая совместимость или происхождение. Любое другое использование может создавать впечатление, что мы сертифицировали или одобрили ваше программное обеспечение.
|
||||
|
||||
### Запрет на доменные имена
|
||||
|
||||
Вы не должны регистрировать доменные имена, содержащие наши словесные знаки или любые их варианты или комбинации.
|
||||
|
||||
## 3. Как отображать наши знаки
|
||||
|
||||
### Использование товарных знаков в тексте
|
||||
|
||||
Всегда используйте товарные знаки в их точной форме с корректным написанием. Их нельзя сокращать, изменять, соединять дефисами или объединять с другими словами таким образом, чтобы это создавало новое название продукта или услуги.
|
||||
|
||||
Недопустимо: Podcop
|
||||
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/
|
||||
8
fe-app-podkop/.prettierrc
Normal file
8
fe-app-podkop/.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
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);
|
||||
});
|
||||
27
fe-app-podkop/eslint.config.js
Normal file
27
fe-app-podkop/eslint.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// eslint.config.js
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
ignores: ['node_modules', 'watch-upload.js'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
];
|
||||
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);
|
||||
1877
fe-app-podkop/locales/calls.json
Normal file
1877
fe-app-podkop/locales/calls.json
Normal file
File diff suppressed because it is too large
Load Diff
1115
fe-app-podkop/locales/podkop.pot
Normal file
1115
fe-app-podkop/locales/podkop.pot
Normal file
File diff suppressed because it is too large
Load Diff
795
fe-app-podkop/locales/podkop.ru.po
Normal file
795
fe-app-podkop/locales/podkop.ru.po
Normal file
@@ -0,0 +1,795 @@
|
||||
# RU translations for PODKOP package.
|
||||
# Copyright (C) 2026 THE PODKOP'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PODKOP package.
|
||||
# divocatt, 2026.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-29 16:40+0300\n"
|
||||
"PO-Revision-Date: 2026-05-29 16:40+0300\n"
|
||||
"Last-Translator: divocatt\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 DNS resolve to get real IP when routing"
|
||||
msgstr "Разрешать домены в реальные IP-адреса перед маршрутизацией в outbound"
|
||||
|
||||
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 HY2 URL: insecure must be 0 or 1"
|
||||
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
|
||||
|
||||
msgid "Invalid HY2 URL: invalid port number"
|
||||
msgstr "Неверный URL Hysteria2: неверный номер порта"
|
||||
|
||||
msgid "Invalid HY2 URL: missing credentials/server"
|
||||
msgstr "Неверный URL Hysteria2: отсутствуют учетные данные/сервер"
|
||||
|
||||
msgid "Invalid HY2 URL: missing host"
|
||||
msgstr "Неверный URL Hysteria2: отсутствует хост"
|
||||
|
||||
msgid "Invalid HY2 URL: missing host & port"
|
||||
msgstr "Неверный URL Hysteria2: отсутствуют хост и порт"
|
||||
|
||||
msgid "Invalid HY2 URL: missing password"
|
||||
msgstr "Неверный URL Hysteria2: отсутствует пароль"
|
||||
|
||||
msgid "Invalid HY2 URL: missing port"
|
||||
msgstr "Неверный URL Hysteria2: отсутствует порт"
|
||||
|
||||
msgid "Invalid HY2 URL: must not contain spaces"
|
||||
msgstr "Неверный URL Hysteria2: не должен содержать пробелов"
|
||||
|
||||
msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://"
|
||||
msgstr "Неверный URL Hysteria2: должен начинаться с hysteria2:// или hy2://"
|
||||
|
||||
msgid "Invalid HY2 URL: obfs-password required when obfs is set"
|
||||
msgstr "Неверный URL Hysteria2: требуется obfs-password, когда установлен obfs"
|
||||
|
||||
msgid "Invalid HY2 URL: parsing failed"
|
||||
msgstr "Неверный URL Hysteria2: ошибка разбора"
|
||||
|
||||
msgid "Invalid HY2 URL: sni cannot be empty"
|
||||
msgstr "Неверный URL Hysteria2: sni не может быть пустым"
|
||||
|
||||
msgid "Invalid HY2 URL: unsupported obfs type"
|
||||
msgstr "Неверный URL Hysteria2: неподдерживаемый тип obfs"
|
||||
|
||||
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 "Log Level"
|
||||
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 "Resolve real IP for routing"
|
||||
msgstr "Разрешение реальных IP-адресов"
|
||||
|
||||
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 log level for sing-box"
|
||||
msgstr "Выберите уровень логов для sing-box"
|
||||
|
||||
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 "Selector"
|
||||
msgstr "Selector"
|
||||
|
||||
msgid "Selector Proxy Links"
|
||||
msgstr "Ссылки прокси для Selector"
|
||||
|
||||
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 "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://, socks4/5://, or hysteria2://hy2://"
|
||||
msgstr "URL должен начинаться с vless://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://"
|
||||
|
||||
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 "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
|
||||
msgstr ""
|
||||
|
||||
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 "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически."
|
||||
40
fe-app-podkop/package.json
Normal file
40
fe-app-podkop/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "fe-app-podkop",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"format": "prettier --write src",
|
||||
"format:js": "prettier --write ../luci-app-podkop/htdocs/luci-static/resources/view/podkop",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||
"build": "tsup src/main.ts",
|
||||
"dev": "tsup src/main.ts --watch",
|
||||
"test": "vitest",
|
||||
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build",
|
||||
"watch:sftp": "node watch-upload.js",
|
||||
"locales:extract-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:extract-calls && yarn locales:generate-pot && yarn locales:generate-po:ru && yarn locales:distribute"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "7.28.4",
|
||||
"@babel/traverse": "7.28.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||
"@typescript-eslint/parser": "8.45.0",
|
||||
"chokidar": "4.0.3",
|
||||
"dotenv": "17.2.3",
|
||||
"eslint": "9.36.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"fast-glob": "3.3.3",
|
||||
"glob": "11.0.3",
|
||||
"prettier": "3.6.2",
|
||||
"ssh2-sftp-client": "12.0.1",
|
||||
"tsup": "8.5.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.45.0",
|
||||
"vitest": "3.2.4"
|
||||
}
|
||||
}
|
||||
110
fe-app-podkop/src/constants.ts
Normal file
110
fe-app-podkop/src/constants.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
export const STATUS_COLORS = {
|
||||
SUCCESS: '#4caf50',
|
||||
ERROR: '#f44336',
|
||||
WARNING: '#ff9800',
|
||||
};
|
||||
|
||||
export const PODKOP_LUCI_APP_VERSION = '__COMPILED_VERSION_VARIABLE__';
|
||||
export const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi';
|
||||
export const IP_CHECK_DOMAIN = 'ip.podkop.fyi';
|
||||
|
||||
export const REGIONAL_OPTIONS = [
|
||||
'russia_inside',
|
||||
'russia_outside',
|
||||
'ukraine_inside',
|
||||
];
|
||||
|
||||
export const ALLOWED_WITH_RUSSIA_INSIDE = [
|
||||
'russia_inside',
|
||||
'meta',
|
||||
'twitter',
|
||||
'discord',
|
||||
'telegram',
|
||||
'cloudflare',
|
||||
'google_ai',
|
||||
'google_play',
|
||||
'hetzner',
|
||||
'ovh',
|
||||
'hodca',
|
||||
'roblox',
|
||||
'digitalocean',
|
||||
'cloudfront',
|
||||
];
|
||||
|
||||
export const DOMAIN_LIST_OPTIONS = {
|
||||
russia_inside: 'Russia inside',
|
||||
russia_outside: 'Russia outside',
|
||||
ukraine_inside: 'Ukraine',
|
||||
geoblock: 'Geo Block',
|
||||
block: 'Block',
|
||||
porn: 'Porn',
|
||||
news: 'News',
|
||||
anime: 'Anime',
|
||||
youtube: 'Youtube',
|
||||
discord: 'Discord',
|
||||
meta: 'Meta',
|
||||
twitter: 'Twitter (X)',
|
||||
hdrezka: 'HDRezka',
|
||||
tiktok: 'Tik-Tok',
|
||||
telegram: 'Telegram',
|
||||
cloudflare: 'Cloudflare',
|
||||
google_ai: 'Google AI',
|
||||
google_play: 'Google Play',
|
||||
hodca: 'H.O.D.C.A',
|
||||
roblox: 'Roblox',
|
||||
hetzner: 'Hetzner ASN',
|
||||
ovh: 'OVH ASN',
|
||||
digitalocean: 'Digital Ocean ASN',
|
||||
cloudfront: 'CloudFront ASN',
|
||||
};
|
||||
|
||||
export const UPDATE_INTERVAL_OPTIONS = {
|
||||
'1h': 'Every hour',
|
||||
'3h': 'Every 3 hours',
|
||||
'12h': 'Every 12 hours',
|
||||
'1d': 'Every day',
|
||||
'3d': 'Every 3 days',
|
||||
};
|
||||
|
||||
export const DNS_SERVER_OPTIONS = {
|
||||
'1.1.1.1': '1.1.1.1 (Cloudflare)',
|
||||
'8.8.8.8': '8.8.8.8 (Google)',
|
||||
'9.9.9.9': '9.9.9.9 (Quad9)',
|
||||
'dns.adguard-dns.com': 'dns.adguard-dns.com (AdGuard Default)',
|
||||
'unfiltered.adguard-dns.com':
|
||||
'unfiltered.adguard-dns.com (AdGuard Unfiltered)',
|
||||
'family.adguard-dns.com': 'family.adguard-dns.com (AdGuard Family)',
|
||||
};
|
||||
export const BOOTSTRAP_DNS_SERVER_OPTIONS = {
|
||||
'77.88.8.8': '77.88.8.8 (Yandex DNS)',
|
||||
'77.88.8.1': '77.88.8.1 (Yandex DNS)',
|
||||
'1.1.1.1': '1.1.1.1 (Cloudflare DNS)',
|
||||
'1.0.0.1': '1.0.0.1 (Cloudflare DNS)',
|
||||
'8.8.8.8': '8.8.8.8 (Google DNS)',
|
||||
'8.8.4.4': '8.8.4.4 (Google DNS)',
|
||||
'9.9.9.9': '9.9.9.9 (Quad9 DNS)',
|
||||
'9.9.9.11': '9.9.9.11 (Quad9 DNS)',
|
||||
};
|
||||
|
||||
export const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds
|
||||
export const CACHE_TIMEOUT = DIAGNOSTICS_UPDATE_INTERVAL - 1000; // 9 seconds
|
||||
export const ERROR_POLL_INTERVAL = 10000; // 10 seconds
|
||||
export const COMMAND_TIMEOUT = 10000; // 10 seconds
|
||||
export const FETCH_TIMEOUT = 10000; // 10 seconds
|
||||
export const BUTTON_FEEDBACK_TIMEOUT = 1000; // 1 second
|
||||
export const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds
|
||||
|
||||
// Command scheduling intervals in diagnostics (in milliseconds)
|
||||
export const COMMAND_SCHEDULING = {
|
||||
P0_PRIORITY: 0, // Highest priority (no delay)
|
||||
P1_PRIORITY: 100, // Very high priority
|
||||
P2_PRIORITY: 300, // High priority
|
||||
P3_PRIORITY: 500, // Above average
|
||||
P4_PRIORITY: 700, // Standard priority
|
||||
P5_PRIORITY: 900, // Below average
|
||||
P6_PRIORITY: 1100, // Low priority
|
||||
P7_PRIORITY: 1300, // Very low priority
|
||||
P8_PRIORITY: 1500, // Background execution
|
||||
P9_PRIORITY: 1700, // Idle mode execution
|
||||
P10_PRIORITY: 1900, // Lowest priority
|
||||
} as const;
|
||||
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);
|
||||
}
|
||||
32
fe-app-podkop/src/helpers/executeShellCommand.ts
Normal file
32
fe-app-podkop/src/helpers/executeShellCommand.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { COMMAND_TIMEOUT } from '../constants';
|
||||
import { withTimeout } from './withTimeout';
|
||||
|
||||
interface ExecuteShellCommandParams {
|
||||
command: string;
|
||||
args: string[];
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
interface ExecuteShellCommandResponse {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code?: number;
|
||||
}
|
||||
|
||||
export async function executeShellCommand({
|
||||
command,
|
||||
args,
|
||||
timeout = COMMAND_TIMEOUT,
|
||||
}: ExecuteShellCommandParams): Promise<ExecuteShellCommandResponse> {
|
||||
try {
|
||||
return withTimeout(
|
||||
fs.exec(command, args),
|
||||
timeout,
|
||||
[command, ...args].join(' '),
|
||||
);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
|
||||
return { stdout: '', stderr: error?.message, code: 0 };
|
||||
}
|
||||
}
|
||||
11
fe-app-podkop/src/helpers/getClashApiUrl.ts
Normal file
11
fe-app-podkop/src/helpers/getClashApiUrl.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function getClashWsUrl(): string {
|
||||
const { hostname } = window.location;
|
||||
|
||||
return `ws://${hostname}:9090`;
|
||||
}
|
||||
|
||||
export function getClashUIUrl(): string {
|
||||
const { hostname } = window.location;
|
||||
|
||||
return `http://${hostname}:9090/ui`;
|
||||
}
|
||||
13
fe-app-podkop/src/helpers/getProxyUrlName.ts
Normal file
13
fe-app-podkop/src/helpers/getProxyUrlName.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function getProxyUrlName(url: string) {
|
||||
try {
|
||||
const [_link, hash] = url.split('#');
|
||||
|
||||
if (!hash) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return decodeURIComponent(hash);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
13
fe-app-podkop/src/helpers/index.ts
Normal file
13
fe-app-podkop/src/helpers/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from './parseValueList';
|
||||
export * from './injectGlobalStyles';
|
||||
export * from './withTimeout';
|
||||
export * from './executeShellCommand';
|
||||
export * from './maskIP';
|
||||
export * from './getProxyUrlName';
|
||||
export * from './onMount';
|
||||
export * from './getClashApiUrl';
|
||||
export * from './splitProxyString';
|
||||
export * from './preserveScrollForPage';
|
||||
export * from './parseQueryString';
|
||||
export * from './svgEl';
|
||||
export * from './insertIf';
|
||||
12
fe-app-podkop/src/helpers/injectGlobalStyles.ts
Normal file
12
fe-app-podkop/src/helpers/injectGlobalStyles.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { GlobalStyles } from '../styles';
|
||||
|
||||
export function injectGlobalStyles() {
|
||||
document.head.insertAdjacentHTML(
|
||||
'beforeend',
|
||||
`
|
||||
<style>
|
||||
${GlobalStyles}
|
||||
</style>
|
||||
`,
|
||||
);
|
||||
}
|
||||
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);
|
||||
}
|
||||
5
fe-app-podkop/src/helpers/maskIP.ts
Normal file
5
fe-app-podkop/src/helpers/maskIP.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function maskIP(ip: string = ''): string {
|
||||
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
||||
|
||||
return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`);
|
||||
}
|
||||
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;
|
||||
}
|
||||
30
fe-app-podkop/src/helpers/onMount.ts
Normal file
30
fe-app-podkop/src/helpers/onMount.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export async function onMount(id: string): Promise<HTMLElement> {
|
||||
return new Promise((resolve) => {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
if (el && el.offsetParent !== null) {
|
||||
return resolve(el);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const target = document.getElementById(id);
|
||||
if (target) {
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
const visible = entries.some((e) => e.isIntersecting);
|
||||
if (visible) {
|
||||
observer.disconnect();
|
||||
io.disconnect();
|
||||
resolve(target);
|
||||
}
|
||||
});
|
||||
|
||||
io.observe(target);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
22
fe-app-podkop/src/helpers/parseQueryString.ts
Normal file
22
fe-app-podkop/src/helpers/parseQueryString.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export function parseQueryString(query: string): Record<string, string> {
|
||||
const clean = query.startsWith('?') ? query.slice(1) : query;
|
||||
|
||||
return clean
|
||||
.split('&')
|
||||
.filter(Boolean)
|
||||
.reduce(
|
||||
(acc, pair) => {
|
||||
const [rawKey, rawValue = ''] = pair.split('=');
|
||||
|
||||
if (!rawKey) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const key = decodeURIComponent(rawKey);
|
||||
const value = decodeURIComponent(rawValue);
|
||||
|
||||
return { ...acc, [key]: value };
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
9
fe-app-podkop/src/helpers/parseValueList.ts
Normal file
9
fe-app-podkop/src/helpers/parseValueList.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function parseValueList(value: string): string[] {
|
||||
return value
|
||||
.split(/\n/) // Split to array by newline separator
|
||||
.map((line) => line.split('//')[0]) // Remove comments
|
||||
.join(' ') // Build clean string
|
||||
.split(/[,\s]+/) // Split to array by comma and space
|
||||
.map((s) => s.trim()) // Remove extra spaces
|
||||
.filter(Boolean); // Leave nonempty items
|
||||
}
|
||||
9
fe-app-podkop/src/helpers/preserveScrollForPage.ts
Normal file
9
fe-app-podkop/src/helpers/preserveScrollForPage.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function preserveScrollForPage(renderFn: () => void) {
|
||||
const scrollY = window.scrollY;
|
||||
|
||||
renderFn();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo({ top: scrollY });
|
||||
});
|
||||
}
|
||||
12
fe-app-podkop/src/helpers/prettyBytes.ts
Normal file
12
fe-app-podkop/src/helpers/prettyBytes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// steal from https://github.com/sindresorhus/pretty-bytes/blob/master/index.js
|
||||
export function prettyBytes(n: number) {
|
||||
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
if (n < 1000) {
|
||||
return n + ' B';
|
||||
}
|
||||
const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
|
||||
n = Number((n / Math.pow(1000, exponent)).toPrecision(3));
|
||||
const unit = UNITS[exponent];
|
||||
return n + ' ' + unit;
|
||||
}
|
||||
3
fe-app-podkop/src/helpers/removeVersionPrefix.ts
Normal file
3
fe-app-podkop/src/helpers/removeVersionPrefix.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function removeVersionPrefix(version: string) {
|
||||
return version.replace(/^v/, '');
|
||||
}
|
||||
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);
|
||||
}
|
||||
7
fe-app-podkop/src/helpers/splitProxyString.ts
Normal file
7
fe-app-podkop/src/helpers/splitProxyString.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function splitProxyString(str: string) {
|
||||
return str
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => !line.startsWith('//'))
|
||||
.filter(Boolean);
|
||||
}
|
||||
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;
|
||||
}
|
||||
42
fe-app-podkop/src/helpers/tests/maskIp.test.js
Normal file
42
fe-app-podkop/src/helpers/tests/maskIp.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { maskIP } from '../maskIP';
|
||||
|
||||
export const validIPs = [
|
||||
['Standard private IP', '192.168.0.1', 'XX.XX.XX.1'],
|
||||
['Public IP', '8.8.8.8', 'XX.XX.XX.8'],
|
||||
['Mixed digits', '10.0.255.99', 'XX.XX.XX.99'],
|
||||
['Edge values', '255.255.255.255', 'XX.XX.XX.255'],
|
||||
['Zeros', '0.0.0.0', 'XX.XX.XX.0'],
|
||||
];
|
||||
|
||||
export const invalidIPs = [
|
||||
['Empty string', '', ''],
|
||||
['Missing octets', '192.168.1', '192.168.1'],
|
||||
['Extra octets', '1.2.3.4.5', '1.2.3.4.5'],
|
||||
['Letters inside', 'abc.def.ghi.jkl', 'abc.def.ghi.jkl'],
|
||||
['Spaces inside', '1. 2.3.4', '1. 2.3.4'],
|
||||
['Just dots', '...', '...'],
|
||||
['IP with port', '127.0.0.1:8080', '127.0.0.1:8080'],
|
||||
['IP with text', 'ip=192.168.0.1', 'ip=192.168.0.1'],
|
||||
];
|
||||
|
||||
describe('maskIP', () => {
|
||||
describe.each(validIPs)('Valid IPv4: %s', (_desc, ip, expected) => {
|
||||
it(`masks "${ip}" → "${expected}"`, () => {
|
||||
expect(maskIP(ip)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidIPs)(
|
||||
'Invalid or malformed IP: %s',
|
||||
(_desc, ip, expected) => {
|
||||
it(`returns original string for "${ip}"`, () => {
|
||||
expect(maskIP(ip)).toBe(expected);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('defaults to empty string if no param passed', () => {
|
||||
expect(maskIP()).toBe('');
|
||||
});
|
||||
});
|
||||
23
fe-app-podkop/src/helpers/withTimeout.ts
Normal file
23
fe-app-podkop/src/helpers/withTimeout.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { logger } from '../podkop';
|
||||
|
||||
export async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
operationName: string,
|
||||
timeoutMessage = _('Operation timed out'),
|
||||
): Promise<T> {
|
||||
let timeoutId;
|
||||
const start = performance.now();
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([promise, timeoutPromise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
const elapsed = performance.now() - start;
|
||||
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' })],
|
||||
);
|
||||
}
|
||||
50
fe-app-podkop/src/luci.d.ts
vendored
Normal file
50
fe-app-podkop/src/luci.d.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
type HtmlTag = keyof HTMLElementTagNameMap;
|
||||
|
||||
type HtmlElement<T extends HtmlTag> = HTMLElementTagNameMap[T];
|
||||
|
||||
type HtmlAttributes<T extends HtmlTag = 'div'> = Partial<
|
||||
Omit<HtmlElement<T>, 'style' | 'children'> & {
|
||||
style?: string | Partial<CSSStyleDeclaration>;
|
||||
class?: string;
|
||||
onclick?: (event: MouseEvent) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
declare global {
|
||||
const fs: {
|
||||
exec(
|
||||
command: string,
|
||||
args?: string[],
|
||||
env?: Record<string, string>,
|
||||
): Promise<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code?: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
const E: <T extends HtmlTag>(
|
||||
type: T,
|
||||
attr?: HtmlAttributes<T> | null,
|
||||
children?: (Node | string)[] | Node | string,
|
||||
) => HTMLElementTagNameMap[T];
|
||||
|
||||
const uci: {
|
||||
load: (packages: string | string[]) => Promise<string>;
|
||||
sections: (conf: string, type?: string, cb?: () => void) => Promise<T>;
|
||||
};
|
||||
|
||||
const _ = (_key: string) => string;
|
||||
|
||||
const ui = {
|
||||
showModal: (_title: stirng, _content: HtmlElement) => undefined,
|
||||
hideModal: () => undefined,
|
||||
addNotification: (
|
||||
_title: string,
|
||||
_children: HtmlElement | HtmlElement[],
|
||||
_className?: string,
|
||||
) => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export {};
|
||||
13
fe-app-podkop/src/main.ts
Normal file
13
fe-app-podkop/src/main.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
if (typeof structuredClone !== 'function')
|
||||
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export * from './validators';
|
||||
export * from './helpers';
|
||||
export * from './podkop';
|
||||
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';
|
||||
3
fe-app-podkop/src/podkop/index.ts
Normal file
3
fe-app-podkop/src/podkop/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './methods';
|
||||
export * from './services';
|
||||
export * from './tabs';
|
||||
@@ -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 || '';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Podkop } from '../../types';
|
||||
|
||||
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
|
||||
return uci.load('podkop').then(() => uci.sections('podkop'));
|
||||
}
|
||||
194
fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts
Normal file
194
fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { getConfigSections } from './getConfigSections';
|
||||
import { Podkop } from '../../types';
|
||||
import { getProxyUrlName, splitProxyString } from '../../../helpers';
|
||||
import { PodkopShellMethods } from '../shell';
|
||||
|
||||
interface IGetDashboardSectionsResponse {
|
||||
success: boolean;
|
||||
data: Podkop.OutboundGroup[];
|
||||
}
|
||||
|
||||
export async function getDashboardSections(): Promise<IGetDashboardSectionsResponse> {
|
||||
const configSections = await getConfigSections();
|
||||
const clashProxies = await PodkopShellMethods.getClashApiProxies();
|
||||
|
||||
if (!clashProxies.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
const proxies = Object.entries(clashProxies.data.proxies).map(
|
||||
([key, value]) => ({
|
||||
code: key,
|
||||
value,
|
||||
}),
|
||||
);
|
||||
|
||||
const data = configSections
|
||||
.filter(
|
||||
(section) =>
|
||||
section.connection_type !== 'block' &&
|
||||
section.connection_type !== 'exclusion' &&
|
||||
section['.type'] !== 'settings',
|
||||
)
|
||||
.map((section) => {
|
||||
if (section.connection_type === 'proxy') {
|
||||
if (section.proxy_config_type === 'url') {
|
||||
const outbound = proxies.find(
|
||||
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||
);
|
||||
|
||||
const activeConfigs = splitProxyString(section.proxy_string);
|
||||
|
||||
const proxyDisplayName =
|
||||
getProxyUrlName(activeConfigs?.[0]) || outbound?.value?.name || '';
|
||||
|
||||
return {
|
||||
withTagSelect: false,
|
||||
code: outbound?.code || section['.name'],
|
||||
displayName: section['.name'],
|
||||
outbounds: [
|
||||
{
|
||||
code: outbound?.code || section['.name'],
|
||||
displayName: proxyDisplayName,
|
||||
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||
type: outbound?.value?.type || '',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (section.proxy_config_type === 'outbound') {
|
||||
const outbound = proxies.find(
|
||||
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||
);
|
||||
|
||||
const parsedOutbound = JSON.parse(section.outbound_json);
|
||||
const parsedTag = parsedOutbound?.tag
|
||||
? decodeURIComponent(parsedOutbound?.tag)
|
||||
: undefined;
|
||||
const proxyDisplayName = parsedTag || outbound?.value?.name || '';
|
||||
|
||||
return {
|
||||
withTagSelect: false,
|
||||
code: outbound?.code || section['.name'],
|
||||
displayName: section['.name'],
|
||||
outbounds: [
|
||||
{
|
||||
code: outbound?.code || section['.name'],
|
||||
displayName: proxyDisplayName,
|
||||
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||
type: outbound?.value?.type || '',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (section.proxy_config_type === 'selector') {
|
||||
const selector = proxies.find(
|
||||
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||
);
|
||||
|
||||
const links = section.selector_proxy_links ?? [];
|
||||
|
||||
const outbounds = links
|
||||
.map((link, index) => ({
|
||||
link,
|
||||
outbound: proxies.find(
|
||||
(item) => item.code === `${section['.name']}-${index + 1}-out`,
|
||||
),
|
||||
}))
|
||||
.map((item) => ({
|
||||
code: item?.outbound?.code || '',
|
||||
displayName:
|
||||
getProxyUrlName(item.link) || item?.outbound?.value?.name || '',
|
||||
latency: item?.outbound?.value?.history?.[0]?.delay || 0,
|
||||
type: item?.outbound?.value?.type || '',
|
||||
selected: selector?.value?.now === item?.outbound?.code,
|
||||
}));
|
||||
|
||||
return {
|
||||
withTagSelect: true,
|
||||
code: selector?.code || section['.name'],
|
||||
displayName: section['.name'],
|
||||
outbounds,
|
||||
};
|
||||
}
|
||||
|
||||
if (section.proxy_config_type === 'urltest') {
|
||||
const selector = proxies.find(
|
||||
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||
);
|
||||
const outbound = proxies.find(
|
||||
(proxy) => proxy.code === `${section['.name']}-urltest-out`,
|
||||
);
|
||||
|
||||
const outbounds = (outbound?.value?.all ?? [])
|
||||
.map((code) => proxies.find((item) => item.code === code))
|
||||
.map((item, index) => ({
|
||||
code: item?.code || '',
|
||||
displayName:
|
||||
getProxyUrlName(section.urltest_proxy_links?.[index]) ||
|
||||
item?.value?.name ||
|
||||
'',
|
||||
latency: item?.value?.history?.[0]?.delay || 0,
|
||||
type: item?.value?.type || '',
|
||||
selected: selector?.value?.now === item?.code,
|
||||
}));
|
||||
|
||||
return {
|
||||
withTagSelect: true,
|
||||
code: selector?.code || section['.name'],
|
||||
displayName: section['.name'],
|
||||
outbounds: [
|
||||
{
|
||||
code: outbound?.code || '',
|
||||
displayName: _('Fastest'),
|
||||
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||
type: outbound?.value?.type || '',
|
||||
selected: selector?.value?.now === outbound?.code,
|
||||
},
|
||||
...outbounds,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (section.connection_type === 'vpn') {
|
||||
const outbound = proxies.find(
|
||||
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||
);
|
||||
|
||||
return {
|
||||
withTagSelect: false,
|
||||
code: outbound?.code || section['.name'],
|
||||
displayName: section['.name'],
|
||||
outbounds: [
|
||||
{
|
||||
code: outbound?.code || section['.name'],
|
||||
displayName: section.interface || outbound?.value?.name || '',
|
||||
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||
type: outbound?.value?.type || '',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
withTagSelect: false,
|
||||
code: section['.name'],
|
||||
displayName: section['.name'],
|
||||
outbounds: [],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
}
|
||||
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,
|
||||
};
|
||||
3
fe-app-podkop/src/podkop/methods/index.ts
Normal file
3
fe-app-podkop/src/podkop/methods/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './custom';
|
||||
export * from './fakeip';
|
||||
export * from './shell';
|
||||
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,
|
||||
),
|
||||
};
|
||||
44
fe-app-podkop/src/podkop/services/core.service.ts
Normal file
44
fe-app-podkop/src/podkop/services/core.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { TabServiceInstance } from './tab.service';
|
||||
import { store } from './store.service';
|
||||
import { logger } from './logger.service';
|
||||
import { PodkopLogWatcher } from './podkopLogWatcher.service';
|
||||
import { PodkopShellMethods } from '../methods';
|
||||
|
||||
export function coreService() {
|
||||
TabServiceInstance.onChange((activeId, tabs) => {
|
||||
logger.info('[TAB]', activeId);
|
||||
store.set({
|
||||
tabService: {
|
||||
current: activeId || '',
|
||||
all: tabs.map((tab) => tab.id),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
5
fe-app-podkop/src/podkop/services/index.ts
Normal file
5
fe-app-podkop/src/podkop/services/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './tab.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');
|
||||
}
|
||||
}
|
||||
167
fe-app-podkop/src/podkop/services/socket.service.ts
Normal file
167
fe-app-podkop/src/podkop/services/socket.service.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { logger } from './logger.service';
|
||||
|
||||
// eslint-disable-next-line
|
||||
type Listener = (data: any) => void;
|
||||
type ErrorListener = (error: Event | string) => void;
|
||||
|
||||
class SocketManager {
|
||||
private static instance: SocketManager;
|
||||
private sockets = new Map<string, WebSocket>();
|
||||
private listeners = new Map<string, Set<Listener>>();
|
||||
private connected = new Map<string, boolean>();
|
||||
private errorListeners = new Map<string, Set<ErrorListener>>();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): SocketManager {
|
||||
if (!SocketManager.instance) {
|
||||
SocketManager.instance = new SocketManager();
|
||||
}
|
||||
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 {
|
||||
if (this.sockets.has(url)) return;
|
||||
|
||||
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.connected.set(url, false);
|
||||
this.listeners.set(url, new Set());
|
||||
this.errorListeners.set(url, new Set());
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
this.connected.set(url, true);
|
||||
logger.info('[SOCKET]', 'Connected to', url);
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (event) => {
|
||||
const handlers = this.listeners.get(url);
|
||||
if (handlers) {
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
handler(event.data);
|
||||
} catch (err) {
|
||||
logger.error('[SOCKET]', `Handler error for ${url}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
this.connected.set(url, false);
|
||||
logger.warn('[SOCKET]', `Disconnected: ${url}`);
|
||||
this.triggerError(url, 'Connection closed');
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (err) => {
|
||||
logger.error('[SOCKET]', `Socket error for ${url}:`, err);
|
||||
this.triggerError(url, err);
|
||||
});
|
||||
}
|
||||
|
||||
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)) {
|
||||
this.connect(url);
|
||||
}
|
||||
|
||||
if (!this.listeners.has(url)) {
|
||||
this.listeners.set(url, new Set());
|
||||
}
|
||||
this.listeners.get(url)?.add(listener);
|
||||
}
|
||||
|
||||
unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||
this.listeners.get(url)?.delete(listener);
|
||||
if (onError) {
|
||||
this.errorListeners.get(url)?.delete(onError);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
send(url: string, data: any): void {
|
||||
const ws = this.sockets.get(url);
|
||||
if (ws && this.connected.get(url)) {
|
||||
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
||||
} else {
|
||||
logger.warn('[SOCKET]', `Cannot send: not connected to ${url}`);
|
||||
this.triggerError(url, 'Not connected');
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(url: string): void {
|
||||
const ws = this.sockets.get(url);
|
||||
if (ws) {
|
||||
ws.close();
|
||||
this.sockets.delete(url);
|
||||
this.listeners.delete(url);
|
||||
this.errorListeners.delete(url);
|
||||
this.connected.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectAll(): void {
|
||||
for (const url of this.sockets.keys()) {
|
||||
this.disconnect(url);
|
||||
}
|
||||
}
|
||||
|
||||
private triggerError(url: string, err: Event | string): void {
|
||||
const handlers = this.errorListeners.get(url);
|
||||
if (handlers) {
|
||||
for (const cb of handlers) {
|
||||
try {
|
||||
cb(err);
|
||||
} catch (e) {
|
||||
logger.error('[SOCKET]', `Error handler threw for ${url}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const socket = SocketManager.getInstance();
|
||||
229
fe-app-podkop/src/podkop/services/store.service.ts
Normal file
229
fe-app-podkop/src/podkop/services/store.service.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { Podkop } from '../types';
|
||||
import { initialDiagnosticStore } from '../tabs/diagnostic/diagnostic.store';
|
||||
|
||||
function jsonStableStringify<T, V>(obj: T): string {
|
||||
return JSON.stringify(obj, (_, value) => {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return Object.keys(value)
|
||||
.sort()
|
||||
.reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = value[key];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, V>,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function jsonEqual<A, B>(a: A, b: B): boolean {
|
||||
try {
|
||||
return jsonStableStringify(a) === jsonStableStringify(b);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
||||
|
||||
// eslint-disable-next-line
|
||||
class StoreService<T extends Record<string, any>> {
|
||||
private value: T;
|
||||
private readonly initial: T;
|
||||
private listeners = new Set<Listener<T>>();
|
||||
private lastHash = '';
|
||||
|
||||
constructor(initial: T) {
|
||||
this.value = initial;
|
||||
this.initial = structuredClone(initial);
|
||||
this.lastHash = jsonStableStringify(initial);
|
||||
}
|
||||
|
||||
get(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set(next: Partial<T>): void {
|
||||
const prev = this.value;
|
||||
const merged = { ...prev, ...next };
|
||||
|
||||
if (jsonEqual(prev, merged)) return;
|
||||
|
||||
this.value = merged;
|
||||
this.lastHash = jsonStableStringify(merged);
|
||||
|
||||
const diff: Partial<T> = {};
|
||||
for (const key in merged) {
|
||||
if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key];
|
||||
}
|
||||
|
||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||
}
|
||||
|
||||
reset<K extends keyof T>(keys?: K[]): void {
|
||||
const prev = this.value;
|
||||
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;
|
||||
|
||||
this.value = next;
|
||||
this.lastHash = jsonStableStringify(next);
|
||||
|
||||
const diff: Partial<T> = {};
|
||||
for (const key in next) {
|
||||
if (!jsonEqual(next[key], prev[key])) diff[key] = next[key];
|
||||
}
|
||||
|
||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||
}
|
||||
|
||||
subscribe(cb: Listener<T>): () => void {
|
||||
this.listeners.add(cb);
|
||||
cb(this.value, this.value, {});
|
||||
return () => this.listeners.delete(cb);
|
||||
}
|
||||
|
||||
unsubscribe(cb: Listener<T>): void {
|
||||
this.listeners.delete(cb);
|
||||
}
|
||||
|
||||
patch<K extends keyof T>(key: K, value: T[K]): void {
|
||||
this.set({ [key]: value } as unknown as Partial<T>);
|
||||
}
|
||||
|
||||
getKey<K extends keyof T>(key: K): T[K] {
|
||||
return this.value[key];
|
||||
}
|
||||
|
||||
subscribeKey<K extends keyof T>(
|
||||
key: K,
|
||||
cb: (value: T[K]) => void,
|
||||
): () => void {
|
||||
let prev = this.value[key];
|
||||
const wrapper: Listener<T> = (val) => {
|
||||
if (!jsonEqual(val[key], prev)) {
|
||||
prev = val[key];
|
||||
cb(val[key]);
|
||||
}
|
||||
};
|
||||
this.listeners.add(wrapper);
|
||||
return () => this.listeners.delete(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
tabService: {
|
||||
current: string;
|
||||
all: string[];
|
||||
};
|
||||
bandwidthWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: { up: number; down: number };
|
||||
};
|
||||
trafficTotalWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: { downloadTotal: number; uploadTotal: number };
|
||||
};
|
||||
systemInfoWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: { connections: number; memory: number };
|
||||
};
|
||||
servicesInfoWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: { singbox: number; podkop: number };
|
||||
};
|
||||
sectionsWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: Podkop.OutboundGroup[];
|
||||
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 = {
|
||||
tabService: {
|
||||
current: '',
|
||||
all: [],
|
||||
},
|
||||
bandwidthWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: { up: 0, down: 0 },
|
||||
},
|
||||
trafficTotalWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: { downloadTotal: 0, uploadTotal: 0 },
|
||||
},
|
||||
systemInfoWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: { connections: 0, memory: 0 },
|
||||
},
|
||||
servicesInfoWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: { singbox: 0, podkop: 0 },
|
||||
},
|
||||
sectionsWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
latencyFetching: false,
|
||||
data: [],
|
||||
},
|
||||
...initialDiagnosticStore,
|
||||
};
|
||||
|
||||
export const store = new StoreService<StoreType>(initialStore);
|
||||
92
fe-app-podkop/src/podkop/services/tab.service.ts
Normal file
92
fe-app-podkop/src/podkop/services/tab.service.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
type TabInfo = {
|
||||
el: HTMLElement;
|
||||
id: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
type TabChangeCallback = (activeId: string | null, allTabs: TabInfo[]) => void;
|
||||
|
||||
export class TabService {
|
||||
private static instance: TabService;
|
||||
private observer: MutationObserver | null = null;
|
||||
private callback?: TabChangeCallback;
|
||||
private lastActiveId: string | null = null;
|
||||
|
||||
private constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
public static getInstance(): TabService {
|
||||
if (!TabService.instance) {
|
||||
TabService.instance = new TabService();
|
||||
}
|
||||
return TabService.instance;
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.observer = new MutationObserver(() => this.handleMutations());
|
||||
this.observer.observe(document.body, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
|
||||
// initial check
|
||||
this.notify();
|
||||
}
|
||||
|
||||
private handleMutations() {
|
||||
this.notify();
|
||||
}
|
||||
|
||||
private getTabsInfo(): TabInfo[] {
|
||||
const tabs = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('.cbi-tab, .cbi-tab-disabled'),
|
||||
);
|
||||
return tabs.map((el) => ({
|
||||
el,
|
||||
id: el.dataset.tab || '',
|
||||
active:
|
||||
el.classList.contains('cbi-tab') &&
|
||||
!el.classList.contains('cbi-tab-disabled'),
|
||||
}));
|
||||
}
|
||||
|
||||
private getActiveTabId(): string | null {
|
||||
const active = document.querySelector<HTMLElement>(
|
||||
'.cbi-tab:not(.cbi-tab-disabled)',
|
||||
);
|
||||
return active?.dataset.tab || null;
|
||||
}
|
||||
|
||||
private notify() {
|
||||
const tabs = this.getTabsInfo();
|
||||
const activeId = this.getActiveTabId();
|
||||
|
||||
if (activeId !== this.lastActiveId) {
|
||||
this.lastActiveId = activeId;
|
||||
this.callback?.(activeId, tabs);
|
||||
}
|
||||
}
|
||||
|
||||
public onChange(callback: TabChangeCallback) {
|
||||
this.callback = callback;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
public getAllTabs(): TabInfo[] {
|
||||
return this.getTabsInfo();
|
||||
}
|
||||
|
||||
public getActiveTab(): string | null {
|
||||
return this.getActiveTabId();
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this.observer?.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const TabServiceInstance = TabService.getInstance();
|
||||
9
fe-app-podkop/src/podkop/tabs/dashboard/index.ts
Normal file
9
fe-app-podkop/src/podkop/tabs/dashboard/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { render } from './render';
|
||||
import { initController } from './initController';
|
||||
import { styles } from './styles';
|
||||
|
||||
export const DashboardTab = {
|
||||
render,
|
||||
initController,
|
||||
styles,
|
||||
};
|
||||
463
fe-app-podkop/src/podkop/tabs/dashboard/initController.ts
Normal file
463
fe-app-podkop/src/podkop/tabs/dashboard/initController.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
import {
|
||||
getClashWsUrl,
|
||||
onMount,
|
||||
preserveScrollForPage,
|
||||
} from '../../../helpers';
|
||||
import { prettyBytes } from '../../../helpers/prettyBytes';
|
||||
import { CustomPodkopMethods, PodkopShellMethods } from '../../methods';
|
||||
import { logger, socket, store, StoreType } from '../../services';
|
||||
import { renderSections, renderWidget } from './partials';
|
||||
import { fetchServicesInfo } from '../../fetchers';
|
||||
import { getClashApiSecret } from '../../methods/custom/getClashApiSecret';
|
||||
|
||||
// Fetchers
|
||||
|
||||
async function fetchDashboardSections() {
|
||||
const prev = store.get().sectionsWidget;
|
||||
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...prev,
|
||||
failed: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data, success } = await CustomPodkopMethods.getDashboardSections();
|
||||
|
||||
if (!success) {
|
||||
logger.error('[DASHBOARD]', 'fetchDashboardSections: failed to fetch');
|
||||
}
|
||||
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
latencyFetching: false,
|
||||
loading: false,
|
||||
failed: !success,
|
||||
data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function connectToClashSockets() {
|
||||
const clashApiSecret = await getClashApiSecret();
|
||||
|
||||
socket.subscribe(
|
||||
`${getClashWsUrl()}/traffic?token=${clashApiSecret}`,
|
||||
(msg) => {
|
||||
const parsedMsg = JSON.parse(msg);
|
||||
|
||||
store.set({
|
||||
bandwidthWidget: {
|
||||
loading: false,
|
||||
failed: false,
|
||||
data: { up: parsedMsg.up, down: parsedMsg.down },
|
||||
},
|
||||
});
|
||||
},
|
||||
(_err) => {
|
||||
logger.error(
|
||||
'[DASHBOARD]',
|
||||
'connectToClashSockets - traffic: failed to connect to',
|
||||
getClashWsUrl(),
|
||||
);
|
||||
store.set({
|
||||
bandwidthWidget: {
|
||||
loading: false,
|
||||
failed: true,
|
||||
data: { up: 0, down: 0 },
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
socket.subscribe(
|
||||
`${getClashWsUrl()}/connections?token=${clashApiSecret}`,
|
||||
(msg) => {
|
||||
const parsedMsg = JSON.parse(msg);
|
||||
|
||||
store.set({
|
||||
trafficTotalWidget: {
|
||||
loading: false,
|
||||
failed: false,
|
||||
data: {
|
||||
downloadTotal: parsedMsg.downloadTotal,
|
||||
uploadTotal: parsedMsg.uploadTotal,
|
||||
},
|
||||
},
|
||||
systemInfoWidget: {
|
||||
loading: false,
|
||||
failed: false,
|
||||
data: {
|
||||
connections: parsedMsg.connections?.length,
|
||||
memory: parsedMsg.memory,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
(_err) => {
|
||||
logger.error(
|
||||
'[DASHBOARD]',
|
||||
'connectToClashSockets - connections: failed to connect to',
|
||||
getClashWsUrl(),
|
||||
);
|
||||
store.set({
|
||||
trafficTotalWidget: {
|
||||
loading: false,
|
||||
failed: true,
|
||||
data: { downloadTotal: 0, uploadTotal: 0 },
|
||||
},
|
||||
systemInfoWidget: {
|
||||
loading: false,
|
||||
failed: true,
|
||||
data: {
|
||||
connections: 0,
|
||||
memory: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Handlers
|
||||
|
||||
async function handleChooseOutbound(selector: string, tag: string) {
|
||||
await PodkopShellMethods.setClashApiGroupProxy(selector, tag);
|
||||
await fetchDashboardSections();
|
||||
}
|
||||
|
||||
async function handleTestGroupLatency(tag: string) {
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...store.get().sectionsWidget,
|
||||
latencyFetching: true,
|
||||
},
|
||||
});
|
||||
|
||||
await PodkopShellMethods.getClashApiGroupLatency(tag);
|
||||
await fetchDashboardSections();
|
||||
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...store.get().sectionsWidget,
|
||||
latencyFetching: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleTestProxyLatency(tag: string) {
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...store.get().sectionsWidget,
|
||||
latencyFetching: true,
|
||||
},
|
||||
});
|
||||
|
||||
await PodkopShellMethods.getClashApiProxyLatency(tag);
|
||||
await fetchDashboardSections();
|
||||
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...store.get().sectionsWidget,
|
||||
latencyFetching: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Renderer
|
||||
|
||||
async function renderSectionsWidget() {
|
||||
logger.debug('[DASHBOARD]', 'renderSectionsWidget');
|
||||
const sectionsWidget = store.get().sectionsWidget;
|
||||
const container = document.getElementById('dashboard-sections-grid');
|
||||
|
||||
if (sectionsWidget.loading || sectionsWidget.failed) {
|
||||
const renderedWidget = renderSections({
|
||||
loading: sectionsWidget.loading,
|
||||
failed: sectionsWidget.failed,
|
||||
section: {
|
||||
code: '',
|
||||
displayName: '',
|
||||
outbounds: [],
|
||||
withTagSelect: false,
|
||||
},
|
||||
onTestLatency: () => {},
|
||||
onChooseOutbound: () => {},
|
||||
latencyFetching: sectionsWidget.latencyFetching,
|
||||
});
|
||||
|
||||
return preserveScrollForPage(() => {
|
||||
container!.replaceChildren(renderedWidget);
|
||||
});
|
||||
}
|
||||
|
||||
const renderedWidgets = sectionsWidget.data.map((section) =>
|
||||
renderSections({
|
||||
loading: sectionsWidget.loading,
|
||||
failed: sectionsWidget.failed,
|
||||
section,
|
||||
latencyFetching: sectionsWidget.latencyFetching,
|
||||
onTestLatency: (tag) => {
|
||||
if (section.withTagSelect) {
|
||||
return handleTestGroupLatency(tag);
|
||||
}
|
||||
|
||||
return handleTestProxyLatency(tag);
|
||||
},
|
||||
onChooseOutbound: (selector, tag) => {
|
||||
handleChooseOutbound(selector, tag);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return preserveScrollForPage(() => {
|
||||
container!.replaceChildren(...renderedWidgets);
|
||||
});
|
||||
}
|
||||
|
||||
async function renderBandwidthWidget() {
|
||||
logger.debug('[DASHBOARD]', 'renderBandwidthWidget');
|
||||
const traffic = store.get().bandwidthWidget;
|
||||
|
||||
const container = document.getElementById('dashboard-widget-traffic');
|
||||
|
||||
if (traffic.loading || traffic.failed) {
|
||||
const renderedWidget = renderWidget({
|
||||
loading: traffic.loading,
|
||||
failed: traffic.failed,
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedWidget = renderWidget({
|
||||
loading: traffic.loading,
|
||||
failed: traffic.failed,
|
||||
title: _('Traffic'),
|
||||
items: [
|
||||
{ key: _('Uplink'), value: `${prettyBytes(traffic.data.up)}/s` },
|
||||
{ key: _('Downlink'), value: `${prettyBytes(traffic.data.down)}/s` },
|
||||
],
|
||||
});
|
||||
|
||||
container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
async function renderTrafficTotalWidget() {
|
||||
logger.debug('[DASHBOARD]', 'renderTrafficTotalWidget');
|
||||
const trafficTotalWidget = store.get().trafficTotalWidget;
|
||||
|
||||
const container = document.getElementById('dashboard-widget-traffic-total');
|
||||
|
||||
if (trafficTotalWidget.loading || trafficTotalWidget.failed) {
|
||||
const renderedWidget = renderWidget({
|
||||
loading: trafficTotalWidget.loading,
|
||||
failed: trafficTotalWidget.failed,
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedWidget = renderWidget({
|
||||
loading: trafficTotalWidget.loading,
|
||||
failed: trafficTotalWidget.failed,
|
||||
title: _('Traffic Total'),
|
||||
items: [
|
||||
{
|
||||
key: _('Uplink'),
|
||||
value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)),
|
||||
},
|
||||
{
|
||||
key: _('Downlink'),
|
||||
value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
async function renderSystemInfoWidget() {
|
||||
logger.debug('[DASHBOARD]', 'renderSystemInfoWidget');
|
||||
const systemInfoWidget = store.get().systemInfoWidget;
|
||||
|
||||
const container = document.getElementById('dashboard-widget-system-info');
|
||||
|
||||
if (systemInfoWidget.loading || systemInfoWidget.failed) {
|
||||
const renderedWidget = renderWidget({
|
||||
loading: systemInfoWidget.loading,
|
||||
failed: systemInfoWidget.failed,
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedWidget = renderWidget({
|
||||
loading: systemInfoWidget.loading,
|
||||
failed: systemInfoWidget.failed,
|
||||
title: _('System info'),
|
||||
items: [
|
||||
{
|
||||
key: _('Active Connections'),
|
||||
value: String(systemInfoWidget.data.connections),
|
||||
},
|
||||
{
|
||||
key: _('Memory Usage'),
|
||||
value: String(prettyBytes(systemInfoWidget.data.memory)),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
async function renderServicesInfoWidget() {
|
||||
logger.debug('[DASHBOARD]', 'renderServicesInfoWidget');
|
||||
const servicesInfoWidget = store.get().servicesInfoWidget;
|
||||
|
||||
const container = document.getElementById('dashboard-widget-service-info');
|
||||
|
||||
if (servicesInfoWidget.loading || servicesInfoWidget.failed) {
|
||||
const renderedWidget = renderWidget({
|
||||
loading: servicesInfoWidget.loading,
|
||||
failed: servicesInfoWidget.failed,
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
|
||||
return container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
const renderedWidget = renderWidget({
|
||||
loading: servicesInfoWidget.loading,
|
||||
failed: servicesInfoWidget.failed,
|
||||
title: _('Services info'),
|
||||
items: [
|
||||
{
|
||||
key: _('Podkop'),
|
||||
value: servicesInfoWidget.data.podkop
|
||||
? _('✔ Enabled')
|
||||
: _('✘ Disabled'),
|
||||
attributes: {
|
||||
class: servicesInfoWidget.data.podkop
|
||||
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
||||
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: _('Sing-box'),
|
||||
value: servicesInfoWidget.data.singbox
|
||||
? _('✔ Running')
|
||||
: _('✘ Stopped'),
|
||||
attributes: {
|
||||
class: servicesInfoWidget.data.singbox
|
||||
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
||||
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
container!.replaceChildren(renderedWidget);
|
||||
}
|
||||
|
||||
async function onStoreUpdate(
|
||||
next: StoreType,
|
||||
prev: StoreType,
|
||||
diff: Partial<StoreType>,
|
||||
) {
|
||||
if (diff.sectionsWidget) {
|
||||
renderSectionsWidget();
|
||||
}
|
||||
|
||||
if (diff.bandwidthWidget) {
|
||||
renderBandwidthWidget();
|
||||
}
|
||||
|
||||
if (diff.trafficTotalWidget) {
|
||||
renderTrafficTotalWidget();
|
||||
}
|
||||
|
||||
if (diff.systemInfoWidget) {
|
||||
renderSystemInfoWidget();
|
||||
}
|
||||
|
||||
if (diff.servicesInfoWidget) {
|
||||
renderServicesInfoWidget();
|
||||
}
|
||||
}
|
||||
|
||||
async function onPageMount() {
|
||||
// Cleanup before mount
|
||||
onPageUnmount();
|
||||
|
||||
// Add new listener
|
||||
store.subscribe(onStoreUpdate);
|
||||
|
||||
// Initial sections fetch
|
||||
await fetchDashboardSections();
|
||||
await fetchServicesInfo();
|
||||
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';
|
||||
@@ -0,0 +1,129 @@
|
||||
import { Podkop } from '../../../types';
|
||||
|
||||
interface IRenderSectionsProps {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
section: Podkop.OutboundGroup;
|
||||
onTestLatency: (tag: string) => void;
|
||||
onChooseOutbound: (selector: string, tag: string) => void;
|
||||
latencyFetching: boolean;
|
||||
}
|
||||
|
||||
function renderFailedState() {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
class: 'pdk_dashboard-page__outbound-section centered',
|
||||
style: 'height: 127px',
|
||||
},
|
||||
E('span', {}, [E('span', {}, _('Dashboard currently unavailable'))]),
|
||||
);
|
||||
}
|
||||
|
||||
function renderLoadingState() {
|
||||
return E('div', {
|
||||
id: 'dashboard-sections-grid-skeleton',
|
||||
class: 'pdk_dashboard-page__outbound-section skeleton',
|
||||
style: 'height: 127px',
|
||||
});
|
||||
}
|
||||
|
||||
export function renderDefaultState({
|
||||
section,
|
||||
onChooseOutbound,
|
||||
onTestLatency,
|
||||
latencyFetching,
|
||||
}: IRenderSectionsProps) {
|
||||
function testLatency() {
|
||||
if (section.withTagSelect) {
|
||||
return onTestLatency(section.code);
|
||||
}
|
||||
|
||||
if (section.outbounds.length) {
|
||||
return onTestLatency(section.outbounds[0].code);
|
||||
}
|
||||
}
|
||||
|
||||
function renderOutbound(outbound: Podkop.Outbound) {
|
||||
function getLatencyClass() {
|
||||
if (!outbound.latency) {
|
||||
return 'pdk_dashboard-page__outbound-grid__item__latency--empty';
|
||||
}
|
||||
|
||||
if (outbound.latency < 800) {
|
||||
return 'pdk_dashboard-page__outbound-grid__item__latency--green';
|
||||
}
|
||||
|
||||
if (outbound.latency < 1500) {
|
||||
return 'pdk_dashboard-page__outbound-grid__item__latency--yellow';
|
||||
}
|
||||
|
||||
return 'pdk_dashboard-page__outbound-grid__item__latency--red';
|
||||
}
|
||||
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''} ${section.withTagSelect ? 'pdk_dashboard-page__outbound-grid__item--selectable' : ''}`,
|
||||
click: () =>
|
||||
section.withTagSelect &&
|
||||
onChooseOutbound(section.code, outbound.code),
|
||||
},
|
||||
[
|
||||
E('b', {}, outbound.displayName),
|
||||
E('div', { class: 'pdk_dashboard-page__outbound-grid__item__footer' }, [
|
||||
E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__outbound-grid__item__type' },
|
||||
outbound.type,
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{ class: getLatencyClass() },
|
||||
outbound.latency ? `${outbound.latency}ms` : 'N/A',
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return E('div', { class: 'pdk_dashboard-page__outbound-section' }, [
|
||||
// Title with test latency
|
||||
E('div', { class: 'pdk_dashboard-page__outbound-section__title-section' }, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
class: 'pdk_dashboard-page__outbound-section__title-section__title',
|
||||
},
|
||||
section.displayName,
|
||||
),
|
||||
latencyFetching
|
||||
? E('div', { class: 'skeleton', style: 'width: 99px; height: 28px' })
|
||||
: E(
|
||||
'button',
|
||||
{
|
||||
class: 'btn dashboard-sections-grid-item-test-latency',
|
||||
click: () => testLatency(),
|
||||
},
|
||||
_('Test latency'),
|
||||
),
|
||||
]),
|
||||
E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__outbound-grid' },
|
||||
section.outbounds.map((outbound) => renderOutbound(outbound)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
export function renderSections(props: IRenderSectionsProps) {
|
||||
if (props.failed) {
|
||||
return renderFailedState();
|
||||
}
|
||||
|
||||
if (props.loading) {
|
||||
return renderLoadingState();
|
||||
}
|
||||
|
||||
return renderDefaultState(props);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
interface IRenderWidgetProps {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
title: string;
|
||||
items: Array<{
|
||||
key: string;
|
||||
value: string;
|
||||
attributes?: {
|
||||
class?: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
function renderFailedState() {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
id: '',
|
||||
style: 'height: 78px',
|
||||
class: 'pdk_dashboard-page__widgets-section__item centered',
|
||||
},
|
||||
_('Currently unavailable'),
|
||||
);
|
||||
}
|
||||
|
||||
function renderLoadingState() {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
id: '',
|
||||
style: 'height: 78px',
|
||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
||||
},
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
function renderDefaultState({ title, items }: IRenderWidgetProps) {
|
||||
return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [
|
||||
E(
|
||||
'b',
|
||||
{ class: 'pdk_dashboard-page__widgets-section__item__title' },
|
||||
title,
|
||||
),
|
||||
...items.map((item) =>
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ''}`,
|
||||
},
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{ class: 'pdk_dashboard-page__widgets-section__item__row__key' },
|
||||
`${item.key}: `,
|
||||
),
|
||||
E(
|
||||
'span',
|
||||
{ class: 'pdk_dashboard-page__widgets-section__item__row__value' },
|
||||
item.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
export function renderWidget(props: IRenderWidgetProps) {
|
||||
if (props.loading) {
|
||||
return renderLoadingState();
|
||||
}
|
||||
|
||||
if (props.failed) {
|
||||
return renderFailedState();
|
||||
}
|
||||
|
||||
return renderDefaultState(props);
|
||||
}
|
||||
54
fe-app-podkop/src/podkop/tabs/dashboard/render.ts
Normal file
54
fe-app-podkop/src/podkop/tabs/dashboard/render.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { renderSections, renderWidget } from './partials';
|
||||
|
||||
export function render() {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
id: 'dashboard-status',
|
||||
class: 'pdk_dashboard-page',
|
||||
},
|
||||
[
|
||||
// Widgets section
|
||||
E('div', { class: 'pdk_dashboard-page__widgets-section' }, [
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-traffic' },
|
||||
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-traffic-total' },
|
||||
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-system-info' },
|
||||
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-service-info' },
|
||||
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||
),
|
||||
]),
|
||||
// All outbounds
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-sections-grid' },
|
||||
renderSections({
|
||||
loading: true,
|
||||
failed: false,
|
||||
section: {
|
||||
code: '',
|
||||
displayName: '',
|
||||
outbounds: [],
|
||||
withTagSelect: false,
|
||||
},
|
||||
onTestLatency: () => {},
|
||||
onChooseOutbound: () => {},
|
||||
latencyFetching: false,
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user