Compare commits

...

664 Commits

Author SHA1 Message Date
Kirill Sobakin
64369a93b0 Merge pull request #263 from kjljxybr/main
Translation update for the installation script
2025-12-03 11:48:57 +03:00
Kirill Sobakin
53a3c943f0 Merge pull request #265 from itdoginfo/fix/service_listen_address
Fix/service listen address
2025-12-03 11:46:53 +03:00
Andrey Petelin
7c7e1c6244 fix: take first LAN IP address and strip CIDR suffix 2025-12-03 10:21:51 +05:00
Andrey Petelin
7fc1f39dd6 fix: have service_listen_address option override automatic detection of listening IP address 2025-12-03 09:58:28 +05:00
Artem Kireev
1c4285dfa8 translations 2025-12-02 10:34:38 +03:00
Kirill Sobakin
ea1273e05e Fix: UDP is lost. Double function call 2025-12-01 23:30:33 +03:00
Kirill Sobakin
5fc3c95928 Merge pull request #262 from itdoginfo/feat/hy2
Feat/hy2
2025-12-01 17:44:40 +03:00
divocat
dd3e70153a fix: correct small points 2025-12-01 16:38:26 +02:00
divocat
622e092317 feat: add hy2 validator 2025-11-30 18:35:06 +02:00
Kirill Sobakin
c045f8f224 Add grpc mode example from #259 2025-11-28 00:44:21 +03:00
Kirill Sobakin
b45088dad7 Merge pull request #259 from kokoc26/main
Feat: service_name for gRPC
2025-11-28 00:43:34 +03:00
Andrey Petelin
82345047cb feat: Add Hysteria2 outbound support 2025-11-26 21:04:46 +05:00
Andrey Petelin
0a4ed367bc refactor: add url_get_scheme and simplify url_get_host/url_get_port using parameter expansion 2025-11-26 21:01:33 +05:00
Andrey Petelin
c3f322ae61 Merge branch 'main' into feat/hy2 2025-11-26 17:06:27 +05:00
Kokoc
eb9239696e feat: add support for optional gRPC service name in outbound transport configuration 2025-11-26 14:52:13 +03:00
Kirill Sobakin
5b3421498e Merge pull request #258 from itdoginfo/refactor/dnsmasq
Refactor/dnsmasq
2025-11-26 14:14:02 +03:00
Andrey Petelin
6a48a060e1 refactor: remove sing-box start exit check 2025-11-26 16:01:41 +05:00
Andrey Petelin
14f704fcb8 fix: use echolog for sing-box start failure 2025-11-26 15:47:12 +05:00
Andrey Petelin
ff43f477e9 chore: restore shutdown_correctly logic 2025-11-26 14:14:27 +05:00
Andrey Petelin
576e58fd17 chore: restore start_main and stop_main; have reload call them instead of full start/stop 2025-11-26 13:56:10 +05:00
Andrey Petelin
d72c98a254 chore: clarify and standardize argument type annotations and optional flags 2025-11-26 10:14:06 +05:00
Andrey Petelin
7a497f1e31 fix: reload PODKOP_CONFIG after uci commit to refresh config on shutdown 2025-11-25 17:05:25 +05:00
Andrey Petelin
d52f6e26ae refactor: add configurable DNS/curl timeouts and retries, detect service proxy, and improve connection checks 2025-11-25 17:04:31 +05:00
Andrey Petelin
68c61aed50 refactor: use uci wrappers 2025-11-25 14:10:18 +05:00
Andrey Petelin
626ac981eb refactor: configuring dnsmasq after starting sing-box 2025-11-25 13:53:24 +05:00
Kirill Sobakin
352d10a047 Fix: HY2 links 2025-11-25 11:32:50 +03:00
Kirill Sobakin
031c419ffb Merge pull request #252 from itdoginfo/fix/argument-list-too-long
jq: Argument list too long
2025-11-24 18:13:43 +03:00
Kirill Sobakin
c13fdf5785 HY2 examples 2025-11-24 18:05:40 +03:00
Andrey Petelin
1b7ab606ba refactor: unify source ruleset preparation and list handlers; make ruleset creation idempotent and atomic updates 2025-11-21 20:37:19 +05:00
Andrey Petelin
2bf208ecac fix: import remote plain domain and subnet lists using chunked processing 2025-11-16 13:21:51 +05:00
Andrey Petelin
e256e4bee5 chore: shorten Text List option label by removing the detailed format hint 2025-11-16 09:56:12 +05:00
Andrey Petelin
32c385b309 fix: load large plain domain/subnet lists in chunks; move ruleset logic to rulesets.sh and nft chunker to nft.sh 2025-11-16 09:55:44 +05:00
Kirill Sobakin
56829c74c8 Merge pull request #246 from itdoginfo/fix/listening_address 2025-11-10 12:58:10 +03:00
Andrey Petelin
9d78cd2ce4 style: add missing semicolons to o.depends calls in luci-app-podkop settings.js 2025-11-06 21:20:05 +05:00
Andrey Petelin
d9ce3b361e chore: correct typo "spedifying" to "specifying" in REST API secret comment 2025-11-06 21:18:15 +05:00
divocat
c67aadf267 feat: add yacd_secret_key support for ws 2025-11-06 16:52:08 +02:00
divocat
ac4d7570f3 feat: add translations for new keys 2025-11-06 16:20:35 +02:00
Andrey Petelin
86897fd0af fix: bind mixed proxy and Clash API to service IP (no 0.0.0.0); add YACD WAN toggle and secret key 2025-11-06 16:33:03 +05:00
Andrey Petelin
230ffbce46 feat: Add optional secret for RESTful API to experimental.clash_api config 2025-11-06 16:30:42 +05:00
Kirill Sobakin
dd5ddd1a14 Merge pull request #240 from itdoginfo/fix/long-nft-command
Import large subnet lists in chunks into nft sets
2025-10-30 16:01:14 +03:00
Andrey Petelin
cc947f9734 fix: import large subnet lists in chunks into nft sets 2025-10-30 14:07:12 +05:00
Kirill Sobakin
f8510cd828 Merge pull request #239 from itdoginfo/fix/crlf-clean
BUG: Clearing CRLF from SRS files
2025-10-29 21:15:47 +03:00
Andrey Petelin
23cbe7be4a fix: include filename in log and remove temp file on CRLF-to-LF conversion 2025-10-29 22:11:29 +05:00
Andrey Petelin
f168fb7e31 refactor: fetch remote JSON to temp files and parse ip_cidr into subnets; remove download_to_stream 2025-10-29 21:52:44 +05:00
Andrey Petelin
fe84b3154f fix: convert Windows CRLF line endings to LF for downloaded files 2025-10-29 21:36:46 +05:00
Kirill Sobakin
d09fdc0b95 Merge pull request #235 from itdoginfo/feat/urltest
feat/urltest
2025-10-27 16:07:13 +03:00
divocat
835cd85970 feat: increase timeouts for delays 2s->5s & 5s -> 10s 2025-10-27 14:56:10 +02:00
divocat
8a3b41ec9c Update fe-app-podkop/locales/podkop.ru.po
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 14:18:36 +02:00
divocat
10d7617739 fix: run linter 2025-10-27 14:15:19 +02:00
divocat
68010ed5f7 Update luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 14:13:31 +02:00
divocat
557e3666eb Update luci-app-podkop/po/ru/podkop.po
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 14:13:20 +02:00
Andrey Petelin
01bff8ccfb chore: refine Russian translations for 'URLTest Testing URL' and 'URLTest Tolerance' 2025-10-27 16:21:51 +05:00
divocat
675a6af89c feat: simplify sb check displaying 2025-10-27 13:16:18 +02:00
divocat
f1a6ff3469 feat: add validations & translations 2025-10-27 13:06:33 +02:00
Andrey Petelin
d4b3377d68 feat: add URLTest check interval, tolerance and testing URL options and wire them into outbound config generation 2025-10-27 15:17:20 +05:00
Kirill Sobakin
d2ef640d76 Merge pull request #233 from itdoginfo/feat/add_display_name
feat: replace outbound code with display name
2025-10-27 12:08:04 +03:00
divocat
47457f2c27 feat: replace outbound code with display name 2025-10-26 16:07:28 +02:00
Kirill Sobakin
8a29e176f2 Merge pull request #232 from itdoginfo/fix/change_json_outbound_validation
small pack of fixes
2025-10-26 16:07:47 +03:00
divocat
9653310208 fix: update locales && possible fix of incorrect outdated 2025-10-26 14:58:09 +02:00
divocat
3540610c78 fix: potential fix of structuredClone for old browsers 2025-10-26 14:52:08 +02:00
divocat
fb54d62a7f feat: actualize json outbound validation 2025-10-26 14:46:39 +02:00
Kirill Sobakin
288b8d4cc2 Merge pull request #230 from itdoginfo/feat/diagnostic-outbound-check
Add outbound check to diagnostic
2025-10-26 09:27:09 +03:00
divocat
e014396ae2 feat: extend selector checks displaying 2025-10-26 01:37:17 +03:00
divocat
694e4ca35a fix: remove extra console log 2025-10-26 01:10:33 +03:00
divocat
788c539e16 feat: add outbounds checks to diagnostics 2025-10-26 01:09:24 +03:00
Kirill Sobakin
743cba8936 Merge pull request #229 from itdoginfo/fix/show-config
Fix masked config
2025-10-25 17:38:45 +03:00
Andrey Petelin
d1d703764c fix: mask outbound_json block and DNS/domain_resolver addresses in podkop config output 2025-10-25 19:27:01 +05:00
Kirill Sobakin
2efd415305 Merge pull request #226 from itdoginfo/fix/excluded_ips
Routing Excluded IPs
2025-10-24 15:34:57 +03:00
Andrey Petelin
407b19b3ed fix: read routing_excluded_ips as non-boolean string with config_get instead of config_get_bool 2025-10-24 17:32:35 +05:00
Kirill Sobakin
c3fac995d5 Merge pull request #224 from itdoginfo/feat/fe-improvements
Some FE improvements
2025-10-23 21:12:03 +03:00
divocat
21ecfbbeca fix: correct types on ru translations 2025-10-23 20:43:46 +03:00
divocat
2918487845 feat: add custom port support to dns 2025-10-23 20:33:18 +03:00
divocat
ac258c53c0 fix: alert displaying 2025-10-23 20:05:55 +03:00
divocat
9a389c47bf fix: actualize locales 2025-10-23 20:02:49 +03:00
divocat
7cd70468c5 feat: add wiki disclaimer to diagnostics 2025-10-23 20:00:55 +03:00
divocat
13d27dab21 feat: add toast when shell exec failed 2025-10-23 19:08:27 +03:00
divocat
9f8f032dce feat: increase shell timeout to 15s 2025-10-23 19:01:06 +03:00
divocat
8301f4c271 feat: update checks displaying 2025-10-23 18:59:23 +03:00
divocat
c4078c8242 feat: update some translations 2025-10-23 18:35:34 +03:00
Kirill Sobakin
e0d149f03a fix 2025-10-23 16:39:41 +03:00
Kirill Sobakin
0f77867ca2 Merge pull request #223 from itdoginfo/fix/version-check
fix: correct versions comparison
2025-10-23 16:21:01 +03:00
divocat
fb5ae9c1e8 fix: correct versions comparison 2025-10-23 16:19:13 +03:00
Kirill Sobakin
9e9bd5a2bd fix: some fixes 2025-10-23 16:15:08 +03:00
Kirill Sobakin
005574a01f feat: rm tiny 2025-10-23 16:14:44 +03:00
Kirill Sobakin
a4bddeb430 feat: logic for 0.7.0 2025-10-23 15:28:53 +03:00
Kirill Sobakin
d335d59f1b Merge pull request #222 from itdoginfo/rc/7.x.x
0.7.0
2025-10-23 14:50:55 +03:00
divocat
272ce012d7 fix: correct build 2025-10-23 14:28:52 +03:00
Kirill Sobakin
64aa28f4e4 feat: upgrade old configuration 2025-10-23 14:27:14 +03:00
Kirill Sobakin
e89f89ea96 fix: nano fix 2025-10-23 14:26:17 +03:00
divocat
8fb8aad53b Update fe-app-podkop/src/validators/validateVlessUrl.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 14:25:00 +03:00
Kirill Sobakin
c1311fdd4b docs: fix 2025-10-23 13:10:13 +03:00
Kirill Sobakin
2cbaa888b2 docs: man for 0.7.0 update 2025-10-23 13:08:02 +03:00
Kirill Sobakin
25bb2355aa fix: rm check_github, check_dnsmasq 2025-10-23 12:47:45 +03:00
divocat
a2eac6f103 fix: disable lan for output network interface 2025-10-23 12:23:51 +03:00
divocat
b5eec292e0 fix: correct nft checks output 2025-10-23 12:23:51 +03:00
Andrey Petelin
5573fce1b1 fix: disable auto_detect_interface when output_network_interface is specified 2025-10-23 14:05:42 +05:00
Kirill Sobakin
a3ac01478f fix: rm check_sing_box_connections 2025-10-23 11:04:23 +03:00
Kirill Sobakin
2fb38286bd fix: for quick start 2025-10-23 11:03:49 +03:00
divocat
ac82cc1770 feat: final translations check 2025-10-21 23:03:20 +03:00
divocat
e8a3725948 feat: translate all keys 2025-10-21 23:02:31 +03:00
divocat
686841c2a1 feat: translate some keys 2025-10-21 22:45:36 +03:00
divocat
3379764ada feat: translate some keys 2025-10-21 21:57:24 +03:00
divocat
1acdbe67a2 feat: implement locales scripts 2025-10-21 21:33:51 +03:00
Andrey Petelin
3bccf8d617 chore: Update UDP-over-TCP option label and description to clarify SOCKS/Shadowsocks applicability 2025-10-21 16:24:24 +05:00
Andrey Petelin
8384e18a22 chore: remove comments support for proxy url 2025-10-21 15:25:07 +05:00
divocat
b78682919a chore: remove comments support for proxy url 2025-10-21 11:32:29 +03:00
Andrey Petelin
e8a5d3d5cc feat: pass default interface to sing-box route 2025-10-21 11:24:44 +05:00
Andrey Petelin
ed7b7e9c6d feat: add optional default_interface parameter and include it in route when provided 2025-10-21 11:23:29 +05:00
divocat
f4be831b5e fix: run prettier for all js files 2025-10-20 22:41:07 +03:00
divocat
4186292aa7 feat: update output_network_interface field logic 2025-10-20 22:40:03 +03:00
divocat
ef70f4e53d feat: add output_network_interface 2025-10-20 21:58:28 +03:00
divocat
f0290fcc9e feat: add socks support 2025-10-20 21:24:08 +03:00
Andrey Petelin
49dd1d608f feat: enable UDP-over-TCP (mode 2) for SOCKS outbound when udp_over_tcp=1 and split args for readability 2025-10-20 20:16:07 +05:00
Andrey Petelin
9c01c8e2dd feat: add socks4/socks4a/socks5 outbound support 2025-10-20 20:09:15 +05:00
Andrey Petelin
d0b06dd829 refactor: rename enable_shadowsocks_udp_over_tcp to enable_udp_over_tcp in config and script 2025-10-20 20:03:13 +05:00
Andrey Petelin
024c258d92 chore: rename SOCKS5 outbound function/comments to generic SOCKS 2025-10-20 20:02:34 +05:00
Andrey Petelin
33b44fd9b3 chore: add SOCKS proxy examples to String-example.md 2025-10-20 20:00:21 +05:00
divocat
8ff9562dcf feat: rename enable_shadowsocks_udp_over_tcp 2025-10-20 17:50:05 +03:00
divocat
9d5cdc3e90 fix: change new log trigger for output 2025-10-20 16:17:57 +03:00
Andrey Petelin
72ad10d737 chore: standardize log messages and severities 2025-10-20 16:58:45 +05:00
Andrey Petelin
e7f3d15bce fix: read dont_touch_dhcp from "settings" section instead of "main" 2025-10-19 19:23:49 +05:00
Andrey Petelin
c0e3e256e3 chore: extract outbound section check into _check_outbound_section and call it via config_foreach 2025-10-19 19:19:59 +05:00
Andrey Petelin
08615b6f04 refactor: use get_first_outbound_section to determine outbound tag, remove SB_MAIN_OUTBOUND_TAG constant 2025-10-19 19:17:50 +05:00
Andrey Petelin
9d4c37b9a2 refactor: create outbound validation into has_outbound_section and check all sections 2025-10-19 18:53:21 +05:00
divocat
13f15dcf11 feat: add pause for log watcher when tab not visible 2025-10-18 23:06:07 +03:00
divocat
213b4603b7 feat: add podkop log watcher with alerts 2025-10-18 23:02:35 +03:00
divocat
f6e347af78 feat: add logger 2025-10-18 22:15:43 +03:00
divocat
7ab0384e0b fix: correct dynamic page behavior on lifecycle events 2025-10-18 01:45:02 +03:00
divocat
4d4164ae6f feat: return community list change handler 2025-10-18 01:16:03 +03:00
divocat
f155d6a118 fix: restore selected value for specific proxy section 2025-10-18 01:11:17 +03:00
divocat
96039f92a9 feat: implement show toast 2025-10-18 01:07:10 +03:00
divocat
fd64eb5bcb feat: add copy & download actions for modal 2025-10-18 00:56:52 +03:00
divocat
d7235e8c06 feat: migrate static texts to locales 2025-10-18 00:44:29 +03:00
divocat
30b30dcca6 feat: integrate additional actions on diagnostics tab 2025-10-18 00:27:06 +03:00
divocat
97ab638b31 feat: actualize dns checks 2025-10-17 23:33:29 +03:00
divocat
7dd3f33284 feat: add vless flow validation for xtls-rprx-vision-udp443 2025-10-17 23:12:37 +03:00
divocat
02a49ed067 Merge remote-tracking branch 'origin/rc/7.x.x' into rc/7.x.x 2025-10-17 23:06:42 +03:00
divocat
af36cf3026 feat: init new partial modal 2025-10-17 23:06:36 +03:00
itdoginfo
cfb821974f refactor: global check #214 2025-10-16 16:49:47 +03:00
divocat
40dac07b29 feat: integrate system info on diagnostic 2025-10-15 22:17:26 +03:00
divocat
d8b7e12c4d feat: add skip next checks if sb is not running 2025-10-15 21:24:56 +03:00
divocat
c0b35c865d feat: actualize system actions behavior 2025-10-15 21:13:52 +03:00
divocat
c35a174708 feat: add dhcp_has_dns_server displaying 2025-10-15 15:40:03 +03:00
divocat
b2a6971700 fix: change command fir start/stop/restart actions 2025-10-15 14:33:46 +03:00
divocat
46ec79e003 fix: change command for enable/disable actions 2025-10-15 13:11:29 +03:00
itdoginfo
d51ac63c94 feat: one method for system info 2025-10-15 11:51:27 +03:00
divocat
53b71ec4b0 fix: change dns_on_router params 2025-10-15 01:21:49 +03:00
divocat
5087be83d3 feat: adapt diagnostics page to mobile 2025-10-15 01:18:32 +03:00
divocat
6772b83861 feat: implement most diagnostics actions 2025-10-15 01:11:30 +03:00
itdoginfo
b8ccb4abfa feat: get latest podkop release 2025-10-14 23:47:31 +03:00
divocat
739e0d2ba7 feat: add some shell methods 2025-10-14 23:15:17 +03:00
divocat
ffa0073441 feat: migrate to proxied clash api methods 2025-10-14 22:36:14 +03:00
divocat
7cd32910d9 refactor: reorganize styles 2025-10-14 22:11:10 +03:00
divocat
67ec5f3090 refactor: unify dynamic page structure 2025-10-14 21:49:09 +03:00
divocat
33dfb8c3f0 refactor: reorganize services 2025-10-14 21:32:06 +03:00
divocat
de3e67f999 refactor: reorganize all methods 2025-10-14 21:27:16 +03:00
divocat
a9fdf286e0 Merge remote-tracking branch 'origin/rc/7.x.x' into rc/7.x.x 2025-10-14 20:17:23 +03:00
divocat
dbf7e39599 feat: implement some diagnostics widget 2025-10-14 20:17:19 +03:00
Andrey Petelin
fa152c3abf feat: honor download_lists_via_proxy and use its outbound section as detour tag for rule set 2025-10-14 20:22:46 +05:00
Andrey Petelin
661ba64879 fix: replace config_get_bool with config_get for community/local/remote list options in podkop script 2025-10-14 20:19:57 +05:00
Andrey Petelin
953b669520 Merge remote-tracking branch 'origin/rc/7.x.x' into rc/7.x.x 2025-10-14 20:19:48 +05:00
Andrey Petelin
3f6f03c8d1 feat: honor download_lists_via_proxy setting and use its outbound section for service mixed inbound routing 2025-10-14 20:12:12 +05:00
divocat
d39ee3a666 feat: add optional minified check displaying 2025-10-14 17:51:14 +03:00
itdoginfo
45bd2d0499 Fix: log function combination 2025-10-14 14:20:05 +03:00
divocat
85b1dc75f5 feat: implement fakeip checks step 2025-10-14 00:26:17 +03:00
divocat
f7517e6794 feat: change naming for rules_other_mark_exist 2025-10-13 23:42:30 +03:00
itdoginfo
2e257e4adf feat: check_fakeip func 2025-10-13 22:43:55 +03:00
divocat
74edbcf07f feat: update diagnostics checks 2025-10-13 22:40:49 +03:00
divocat
aea6fd9453 feat: change dns check output 2025-10-13 21:49:38 +03:00
divocat
0fba31c10a feat: change icons for diagnostic 2025-10-13 21:32:01 +03:00
divocat
a7150f7143 feat: add download_lists_via_proxy_section 2025-10-13 21:05:48 +03:00
itdoginfo
44894f3257 Fix path 2025-10-12 18:53:22 +03:00
itdoginfo
f20e205b72 shellcheck fix 2025-10-12 18:49:09 +03:00
itdoginfo
7a2868b630 Add CI for shellcheck 2025-10-12 16:25:03 +03:00
itdoginfo
55df0f283d Added clash_api func. Some fixes 2025-10-12 14:55:57 +03:00
divocat
e3e0b2d4e4 feat: implement fake ip check mock 2025-10-11 23:21:53 +03:00
divocat
4334643e8e feat: implement base of diagnostics 2025-10-11 23:09:31 +03:00
divocat
5486dfb0a4 feat: add getSingBoxCheck js method 2025-10-11 20:17:24 +03:00
itdoginfo
fd0b981186 Fix check_nft_rules. Add check_sing_box func 2025-10-11 18:57:55 +03:00
divocat
d041334d88 feat: add getDNSCheck & getNftRulesCheck js methods 2025-10-11 17:48:53 +03:00
itdoginfo
791cc1c945 Diagnostics: add check_nft_rules 2025-10-11 14:36:04 +03:00
itdoginfo
63d56e736d Added init.d dir for sync 2025-10-11 14:35:14 +03:00
itdoginfo
a33b53743f Switch to sing-box-tiny. Add bind-dig depends 2025-10-11 00:33:20 +03:00
itdoginfo
3d12327868 Switch DNS check to dig. New checks and output format for check_dns_available 2025-10-11 00:32:33 +03:00
divocat
1bdd49e198 fix: adapt dashboard for new sections structure 2025-10-10 20:49:44 +03:00
divocat
b90f520c68 feat: add bulk watch for fe/bin/lib directories 2025-10-10 20:39:28 +03:00
Andrey Petelin
7bfb673b49 refactor: restructure podkop config 2025-10-10 20:14:53 +03:00
Andrey Petelin
ee93c26098 fix: Use connection_type instead of mode for option dependencies in podkop section.js 2025-10-10 20:14:51 +03:00
Andrey Petelin
f95d801d44 refactor: rename 'ss_uot' to 'enable_shadowsocks_udp_over_tcp' 2025-10-10 20:14:49 +03:00
Andrey Petelin
ca5a3a79fe refactor: rename 'procd_reload_delay' to 'badwan_reload_delay' 2025-10-10 20:14:35 +03:00
Andrey Petelin
f128bc4ec7 refactor: rename 'restart_ifaces' to 'badwan_monitored_interfaces' 2025-10-10 20:14:34 +03:00
Andrey Petelin
458fd9251a refactor: rename 'mon_restart_ifaces' to 'enable_badwan_interface_monitoring' 2025-10-10 20:14:32 +03:00
Andrey Petelin
35d9441837 refactor: rename 'detour' to 'download_lists_via_proxy' 2025-10-10 20:14:30 +03:00
Andrey Petelin
e3557f374e refactor: rename 'quic_disabled' to 'disable_quic' 2025-10-10 20:14:27 +03:00
Andrey Petelin
1e6b555bfa refactor: rename 'yacd' to 'enable_yacd' 2025-10-10 20:14:26 +03:00
Andrey Petelin
036808917d refactor: rename 'iface' to 'source_network_interfaces' 2025-10-10 20:14:23 +03:00
Andrey Petelin
687334bf8d refactor: rename config key 'mode' to 'connection_type' 2025-10-10 20:14:21 +03:00
Andrey Petelin
095b3c6fa9 chore: improve wording and capitalization of settings UI labels and descriptions 2025-10-10 20:14:19 +03:00
Andrey Petelin
ba69e3eacc refactor: use list presence instead of *_enabled flags, simplify UI texts/placeholders, remove mixed inbound tag 2025-10-10 20:14:17 +03:00
Andrey Petelin
9be0eb3e57 refactor: rename all_traffic_ip to fully_routed_ips, remove all_traffic_from_ip_enabled flag, update handlers 2025-10-10 20:14:15 +03:00
Andrey Petelin
d3847db313 feat: Add mixed proxy per section with UI port option and sing-box integration 2025-10-10 20:14:13 +03:00
Andrey Petelin
ba91c180e8 refactor: switch UCI lookups from 'main' to 'settings', add routing_excluded_ips and relocate update_interval in UI 2025-10-10 20:14:10 +03:00
Andrey Petelin
8a80df9dc0 refactor: Pass 'section' to config_foreach so outbound handler iterates only the correct sections 2025-10-10 20:14:08 +03:00
Andrey Petelin
d2f0de39d9 refactor: remove legacy migration logic; make migration() a no-op 2025-10-10 20:14:05 +03:00
divocat
e662f25f53 fix: reorder options on settings tab 2025-10-10 20:14:00 +03:00
divocat
3042a86412 feat: init diagnostic tab 2025-10-10 20:13:56 +03:00
divocat
9f1505db48 fix: reorder options on settings tab 2025-10-10 20:13:53 +03:00
divocat
34404f6e40 feat: reorder section & settings tabs 2025-10-10 20:13:49 +03:00
divocat
9e0135983f fix: change yacd url for option 2025-10-10 20:13:45 +03:00
divocat
d176f24a7f chore: change podkop config luci builder 2025-10-10 20:12:43 +03:00
itdoginfo
acd1ca1bcb Update Readme 2025-10-10 15:35:25 +03:00
Kirill Sobakin
984ae5f2a9 Merge pull request #213 from itdoginfo/fix/diagnostic
fix: correct luci-version displaying
2025-10-10 14:46:59 +03:00
divocat
7a62898541 fix: correct luci-version displaying 2025-10-10 14:41:35 +03:00
itdoginfo
7911d1d29f Draft false 2025-10-10 14:23:37 +03:00
Kirill Sobakin
bc673b7881 Merge pull request #212 from itdoginfo/fix/dashboard
fix: correct vless/trojan validation on some browsers
2025-10-10 14:21:25 +03:00
divocat
0493565c5f fix: implement query params parsing func 2025-10-10 14:06:19 +03:00
itdoginfo
4cd1094395 Check ipk without v 2025-10-09 19:53:37 +03:00
itdoginfo
e87b431d86 Check v* 2025-10-09 19:33:27 +03:00
itdoginfo
b9ee917abf Fix PODKOP_VERSION for ipk 2025-10-09 19:20:35 +03:00
divocat
715a278af8 fix: force http for yacd enable link 2025-10-09 18:23:35 +03:00
divocat
9bc2b5ffef fix: correct link validation & some points on dash 2025-10-09 18:23:35 +03:00
divocat
9d89258c0c fix: correct vless/trojan validation on some browsers 2025-10-09 18:23:35 +03:00
itdoginfo
52d1c5d95f Fix PKG_VERSION -> PODKOP_VERSION 2025-10-09 18:15:54 +03:00
itdoginfo
587e5245d3 Test without v* 2025-10-09 16:01:31 +03:00
itdoginfo
e7578d61bc Rm v* 2025-10-09 15:34:23 +03:00
itdoginfo
9918b71a82 Fix version tag 3 2025-10-09 15:32:01 +03:00
itdoginfo
f48c4ff2bb Fix version tag 2 2025-10-09 15:08:37 +03:00
itdoginfo
e77bcc386a Fix version tag 2025-10-09 14:59:22 +03:00
itdoginfo
455c19ab2e Fix #211 2025-10-09 14:40:45 +03:00
Kirill Sobakin
914e1792f3 Merge pull request #211 from SaltyMonkey/build-process-improvements
chore: Build process improvements
2025-10-09 11:13:59 +03:00
SaltyMonkey
826245a89a fix: Minor changes and bugfixes, ci fix 2025-10-09 00:56:15 +03:00
SaltyMonkey
b5cfc017fe chore: Automatic build process rewrite
* Added apk packages support
* Move to matrix builds
* Minor versions update for some actions just in case
* Automatic release with ipk/apk packages
2025-10-08 23:15:52 +03:00
SaltyMonkey
267fd2b793 refactor: Added .gitattributes for better dev life at win and linux 2025-10-08 22:26:16 +03:00
SaltyMonkey
c0b400dfb0 refactor: New docker files for build process 2025-10-08 22:25:06 +03:00
SaltyMonkey
752636347e refactor: Remove old docker files 2025-10-08 22:20:33 +03:00
SaltyMonkey
28aeb29c51 refactor: Update luci-app-podkop package
* Removed direct package manager calls
* Removed commands related to optional luci package
* Update external global_check call with version pass
* Removed useless external calls in version check cases
* Improved build process support: version will be automatically set at installation time from package metadata and will be readable from JS as constant
2025-10-08 22:09:48 +03:00
SaltyMonkey
6ff543d7fb refactor: Update podkop package
* Removed direct package manager calls
* Removed commands related to optional luci package
* Added optional parameter for global_check for cases when function called by LuCI package
* Removed useless external calls in version check cases
* Improved build process support: version will be automatically set at installation time from package metadata and will be readable from script itself
2025-10-08 21:57:46 +03:00
SaltyMonkey
b89fe33296 chore: Added apk package manager support for install script 2025-10-08 21:42:19 +03:00
Kirill Sobakin
3d63a82815 Merge pull request #209 from itdoginfo/fix/dashboard
fix: force http for clash api
2025-10-08 00:05:04 +03:00
divocat
934f802879 fix: force http for clash api 2025-10-08 00:02:41 +03:00
Kirill Sobakin
4d0755e4c0 Merge pull request #208 from itdoginfo/fix/dashboard
feat: change get latency class coloring
2025-10-07 23:48:29 +03:00
divocat
88ee7b4a54 feat: change get latency class coloring 2025-10-07 23:45:07 +03:00
Kirill Sobakin
0eb575d171 Merge pull request #207 from itdoginfo/fix/dashboard
fix: dashboard behavior on corner cases
2025-10-07 23:41:06 +03:00
divocat
9a46d731c9 fix: correct dashboard displaying 2025-10-07 23:33:57 +03:00
divocat
a45ab62885 fix: correct section display name for json outbound 2025-10-07 22:56:40 +03:00
Kirill Sobakin
b7bad57299 Merge pull request #206 from itdoginfo/feat/all_traffic_ip_cidr
feat: add support IP/CIDR format in LuCI for all_traffic_ip
2025-10-07 22:33:43 +03:00
divocat
4ac755bd36 feat: add support IP/CIDR format in LuCI for all_traffic_ip 2025-10-07 22:26:29 +03:00
Kirill Sobakin
e9a0c96882 Merge pull request #205 from itdoginfo/hotfix
Hotfix
2025-10-07 21:34:26 +03:00
divocat
48c8f01d2f fix: correct proxy string label displaying on dashboard 2025-10-07 20:34:38 +03:00
divocat
72b2a34af9 fix: allow .tld for user_domains_text & user_domains 2025-10-07 19:19:10 +03:00
Andrey Petelin
ae4a3781e6 i18n: update Russian translations for additional settings and related messages 2025-10-07 20:42:34 +05:00
divocat
1bce7c0c98 fix: migrate test latency to locales 2025-10-07 18:26:59 +03:00
Andrey Petelin
a8b2001cc1 fix: sort input files before processing in xgettext.sh to ensure consistent POT generation 2025-10-07 20:12:46 +05:00
Andrey Petelin
d6481675e0 fix: update shebang to env bash and add strict mode for safer script execution in xgettext.sh 2025-10-07 20:12:14 +05:00
Kirill Sobakin
2ba1c2f740 Merge pull request #188 from itdoginfo/feat/fe-app-podkop
feat: Introduce new fe modular format with minimal scope of refactoring
2025-10-07 17:38:19 +03:00
divocat
5d0f8ce5bf fix: resolve copilot suggestions 2025-10-07 17:23:26 +03:00
divocat
ddad137fc1 Merge branch 'feat/yacd-exp' into feat/fe-app-podkop 2025-10-07 17:16:36 +03:00
divocat
7b2e5d2838 feat: add missing locales 2025-10-07 17:14:28 +03:00
divocat
9a72785fa7 feat: migrate to _ locales handler 2025-10-07 16:55:50 +03:00
divocat
e0874c3775 refactor: make dashboard widgets reactive 2025-10-07 16:26:06 +03:00
divocat
1e6c827f2b fix: cleanup global styles 2025-10-07 01:07:48 +03:00
divocat
c8c0025470 feat: set clash delay timeout to 5s 2025-10-07 01:07:48 +03:00
divocat
c78f97d64f fix: run prettier & remove unused fragments 2025-10-07 01:07:48 +03:00
divocat
7cb43ffb65 feat: implement dashboard tab 2025-10-07 01:07:48 +03:00
divocat
1e4cda9400 feat: add loaders to test latency buttons 2025-10-07 01:07:48 +03:00
divocat
caf82b096f feat: add test latency & select tag functionality 2025-10-07 01:07:48 +03:00
divocat
6117b0ef9b feat: colorize status ans latency 2025-10-07 01:07:48 +03:00
Andrey Petelin
5418187dd3 feat: enable Clash API with YACD or online mode in podkop configuration 2025-10-07 01:07:48 +03:00
Andrey Petelin
31b09cc3d2 feat: conditionally include external_ui in clash_api config if external_ui path is provided 2025-10-07 01:07:48 +03:00
divocat
b2a473573b feat: add vpn section outbound displaying 2025-10-06 15:13:55 +03:00
divocat
aad6d8c002 feat: implement dashboard prototype 2025-10-06 03:43:55 +03:00
divocat
c75dd3e78b feat: add base clash api methods 2025-10-05 18:36:39 +03:00
divocat
341f260fcf refactor: change vless validation logic 2025-10-05 18:13:19 +03:00
divocat
c5e19a0f2d fix: remove unused params for url test string 2025-10-05 16:59:02 +03:00
divocat
d50b6dbab6 fix: correct output format for test 2025-10-05 16:37:56 +03:00
divocat
99c8ead148 fix: correct output format for test
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-05 16:17:35 +03:00
divocat
d605094a9d Merge remote-tracking branch 'origin/feat/fe-app-podkop' into feat/fe-app-podkop 2025-10-05 16:13:08 +03:00
divocat
eb60e6edec fix: run prettier for luci app js assets 2025-10-05 16:12:56 +03:00
itdoginfo
08f5b31d58 CI: Add great succces to summary 2025-10-04 13:19:03 +03:00
itdoginfo
f69e3478c8 CI: Add frontend workflow 2025-10-04 12:47:39 +03:00
divocat
d9a4f50f62 feat: finalize first modular pack 2025-10-04 01:15:12 +03:00
divocat
eb52d52eb4 feat: implement validateProxyUrl validation 2025-10-03 21:44:42 +03:00
divocat
3f4a0cf094 feat: make URLTest Proxy Links options textarea 2025-10-03 21:16:38 +03:00
Kirill Sobakin
b0a8526c90 Merge pull request #187 from itdoginfo/hotfix
Fix DNS server address validation
2025-10-03 16:17:29 +03:00
Andrey Petelin
e9d5b18816 fix: resolve domain resolver DNS server address before IPv4 validation in VPN and DNS configuration sections 2025-10-03 18:07:24 +05:00
divocat
7b06f422af feat: add trojan link support to Proxy Configuration URL validation 2025-10-03 14:32:17 +03:00
divocat
96bcc36cf1 refactor: remove unused variables 2025-10-03 14:12:08 +03:00
divocat
db8e8e8298 refactor: migrate global styles to injectGlobalStyles 2025-10-03 14:12:08 +03:00
divocat
eb0617eef1 fix: corrent naming for User Domains List validation 2025-10-03 14:12:08 +03:00
divocat
8f9bff9a64 refactor: migrate Outbound Configuration validation to modular 2025-10-03 14:12:08 +03:00
divocat
65d3a9253f refactor: migrate Proxy Configuration URL validation to modular 2025-10-03 14:12:08 +03:00
divocat
b99116fbf3 feat: implement ss/vless validations 2025-10-03 14:12:08 +03:00
divocat
8f19f31e7a refactor: migrate User Domains List validation to modular 2025-10-03 14:12:08 +03:00
divocat
327c3d2b68 feat: implement parseValueList helper 2025-10-03 14:12:08 +03:00
divocat
260b7b9558 refactor: migrate User Subnets List validation to modular 2025-10-03 14:12:08 +03:00
divocat
df9dba9742 feat: implement bulk validate 2025-10-03 14:12:08 +03:00
divocat
547feb0e06 feat: implement validateSubnet 2025-10-03 14:12:08 +03:00
divocat
77e141b305 feat: add soft wrap to Proxy Configuration URL textarea 2025-10-03 14:12:08 +03:00
divocat
cfc5d995a8 refactor: change Network Interface filter logic 2025-10-03 14:12:08 +03:00
divocat
e84233a10c refactor: change Interface for monitoring filter logic 2025-10-03 14:12:08 +03:00
divocat
b71c7b379d refactor: change Source Network Interface filter logic 2025-10-03 14:12:08 +03:00
divocat
3988588c9f feat: migrate yacd url to dynamic 2025-10-03 14:12:08 +03:00
divocat
cd133838cb feat: add BOOTSTRAP_DNS_SERVER_OPTIONS to constants 2025-10-03 14:12:08 +03:00
divocat
f58472a53d feat: migrate validatePath to modular 2025-10-03 14:12:08 +03:00
divocat
5e95148492 feat: migrate constants to modular 2025-10-03 14:12:08 +03:00
divocat
df9400514b feat: migrate some validation places of additional tab to modular 2025-10-03 14:12:08 +03:00
divocat
14eec8e600 feat: migrate some validation places of config sections to modular 2025-10-03 14:12:08 +03:00
divocat
294cb21e91 feat: Introduce fe modular build system 2025-10-03 14:12:08 +03:00
Kirill Sobakin
4ef15f7340 Merge pull request #186 from itdoginfo/trojan
Trojan
2025-10-03 14:10:33 +03:00
Andrey Petelin
41563a5828 fix: correct Russian translation for "works on router" in podkop.po file 2025-10-03 16:08:25 +05:00
Andrey Petelin
2e99ee3a17 fix: pass outbound tag to security and transport functions for accurate config updates 2025-10-03 16:03:36 +05:00
Andrey Petelin
a8db33dd28 feat: add Trojan proxy support (#172) 2025-10-03 15:40:16 +05:00
Andrey Petelin
1295e0dcb2 feat: add function to append Trojan outbound to sing-box JSON configuration 2025-10-03 14:19:45 +05:00
Andrey Petelin
b6bec0fc51 refactor: rename VLESS-specific functions to generic outbound transport and TLS setters 2025-10-03 13:45:00 +05:00
Andrey Petelin
769d263be2 chore: update String-example.md with detailed Shadowsocks, VLESS, and Trojan protocol examples and configurations 2025-10-03 13:35:18 +05:00
Kirill Sobakin
470f11699c Merge pull request #184 from itdoginfo/split_dns
Replacing Split DNS with Domain Resolver and Bootstrap DNS
2025-10-02 18:18:28 +03:00
Andrey Petelin
852b6c043a i18n: update Russian translations and template with new DNS and domain resolver entries 2025-10-02 19:52:21 +05:00
Andrey Petelin
f5cafd5573 chore: add --no-location option to msgmerge and msginit to omit source code references in PO files 2025-10-02 19:44:39 +05:00
Andrey Petelin
3562b913a2 chore: update DNS protocol and server field labels 2025-10-02 19:35:40 +05:00
Andrey Petelin
f4ac9dcc77 feat: add domain resolver support to VPN mode 2025-10-02 17:49:23 +05:00
Andrey Petelin
f5a629afcf feat: add optional domain_resolver parameter to interface outbound config function 2025-10-02 17:48:08 +05:00
Andrey Petelin
aea201bf24 fix: replace non-working split DNS with bootstrap DNS for upstream DNS resolution 2025-10-02 15:58:26 +05:00
Kirill Sobakin
1313c3b26f Merge pull request #182 from itdoginfo/translation
Translation
2025-10-02 10:54:26 +03:00
Andrey Petelin
a3f4e942c3 chore: update Russian translation file encoding to UTF-8 and reformat multiline strings for better readability 2025-10-02 11:17:33 +05:00
Andrey Petelin
4d8e4c1c13 chore: set width variable to 120 for consistent msgmerge and xgettext formatting in localization scripts 2025-10-02 11:16:50 +05:00
itdoginfo
0cb5c2daae Stop podkop before update sing-box 2025-10-01 14:38:32 +03:00
Kirill Sobakin
19fbfff555 Merge pull request #180 from itdoginfo/translation
Translation
2025-10-01 10:33:57 +03:00
Kirill Sobakin
75a2ed1e29 Merge pull request #179 from itdoginfo/fix
refactor: Add version checks and service existence validation
2025-09-30 19:26:45 +03:00
Andrey Petelin
759b6748c6 refactor: Replace opkg version checks with direct command execution 2025-09-30 19:55:25 +05:00
Andrey Petelin
0a27784f85 chore: Update translation template and Russian translations 2025-09-30 19:30:23 +05:00
Andrey Petelin
3b95ac2bc3 chore: Add width option and package name to xgettext and msgmerge scripts 2025-09-30 19:29:46 +05:00
Andrey Petelin
5c51d99d73 chore: Improve NTP exclusion option description for clarity 2025-09-30 13:25:12 +05:00
Andrey Petelin
904b90e012 fix: Remove empty string translations from UI labels 2025-09-30 13:06:52 +05:00
Andrey Petelin
5fb8343cf8 fix: Remove translation function from Yacd link in additional settings tab 2025-09-30 13:05:56 +05:00
Andrey Petelin
014f0f4bdf feat: Add scripts for generating and updating translation templates 2025-09-30 13:04:44 +05:00
Andrey Petelin
dd44e0156e fix: restore default cachesize and noresolv values in dnsmasq configuration if unset 2025-09-27 12:22:50 +05:00
Andrey Petelin
927b8a53b0 fix: restore default resolvfile in DNS settings if backup servers are missing to prevent resolution issues 2025-09-27 11:47:01 +05:00
itdoginfo
7ba20905d5 Fix sing-box remove 2025-09-26 13:21:53 +03:00
Andrey Petelin
5b15a56502 fix: Add local declaration for lowest variable and improve opkg status error redirection spacing 2025-09-25 11:43:03 +05:00
Andrey Petelin
c31df68bec refactor: Add version checks and service existence validation for required packages before starting podkop 2025-09-25 11:11:41 +05:00
Kirill Sobakin
0a5229f4f6 Merge pull request #173 from itdoginfo/fix
fix: Remove URL fragment before parsing VLESS links
2025-09-18 11:13:50 +03:00
Andrey Petelin
5ecb6ef997 fix: Remove URL fragment before parsing VLESS links 2025-09-18 12:59:17 +05:00
Kirill Sobakin
340c2b3505 Merge pull request #171 from itdoginfo/fix
Fix
2025-09-17 19:17:58 +03:00
Andrey Petelin
515c0be38b fix: revert changes from issue #148 2025-09-17 21:14:57 +05:00
Andrey Petelin
59c59bcb17 fix: Improve shadowsocks userinfo decoding with format validation and error handling` 2025-09-17 21:09:03 +05:00
Kirill Sobakin
e5eff41a0f Merge pull request #170 from itdoginfo/fix
Fix: Mask urltest_proxy_links and move sing-box config check to init config function
2025-09-17 13:04:32 +03:00
Andrey Petelin
bb1c06951c fix: Exclusion of ruleset subnets from dns rules (#148) 2025-09-17 13:31:00 +05:00
Andrey Petelin
4999840340 fix: Support comments in user domain/subnet parsing 2025-09-17 11:58:55 +05:00
Andrey Petelin
6c5a271105 fix: Move sing-box config check to after temp file creation 2025-09-16 20:11:13 +05:00
Andrey Petelin
e336bb831c fix: Mask urltest_proxy_links in config output 2025-09-16 19:45:32 +05:00
Kirill Sobakin
00db99723c Merge pull request #169 from itdoginfo/urltest
fix: Correct boolean value for interrupt_exist_connections in JSON
2025-09-16 15:14:43 +03:00
Andrey Petelin
5439504de7 fix: Correct boolean value for interrupt_exist_connections in JSON generation 2025-09-16 17:12:19 +05:00
Kirill Sobakin
c3072162de Merge pull request #168 from itdoginfo/urltest
feat: Add URLTest proxy configuration type with dynamic list support
2025-09-16 15:10:37 +03:00
Andrey Petelin
d021636f85 chore: Fix placeholder text typo in proxy links field 2025-09-16 17:09:37 +05:00
Andrey Petelin
a06aac0613 feat: Add URLTest proxy configuration type with dynamic list support 2025-09-16 16:58:39 +05:00
Kirill Sobakin
29159243ea Merge pull request #167 from itdoginfo/fix
Fix
2025-09-16 11:44:27 +03:00
Andrey Petelin
269123600a fix: Correct variable usage in domain/subnet parsing function (#165) 2025-09-16 13:39:40 +05:00
Andrey Petelin
49add27f81 fix: Improve domain validation to support suffix matching (#166) 2025-09-16 13:19:02 +05:00
itdoginfo
c929c74da5 DeepWiki 2025-09-15 23:29:04 +03:00
itdoginfo
bb91144a91 Update 2025-09-15 22:22:10 +03:00
itdoginfo
2291d9fb9d Check 23.05 2025-09-15 22:15:10 +03:00
Kirill Sobakin
f722a513d0 Merge pull request #163 from itdoginfo/fix
fix: Use correct variable for detour service address
2025-09-15 17:27:15 +03:00
Andrey Petelin
a71707f174 fix: Use correct variable for detour service address 2025-09-15 19:22:52 +05:00
Kirill Sobakin
983f05345b Merge pull request #161 from itdoginfo/refactoring
Refactoring
2025-09-15 15:52:22 +03:00
Andrey Petelin
ee246895de fix: Redirect base64 decode errors to /dev/null 2025-09-15 17:41:17 +05:00
Andrey Petelin
27719f90ee feat: Add support for DoH URLs with paths and UDP port specification 2025-09-14 17:51:20 +05:00
Andrey Petelin
4a17cf66a3 refactor: Add file existence checks and improve startup reliability 2025-09-14 09:26:26 +05:00
Andrey Petelin
db956452d1 refactor: Move logging functions to library file 2025-09-14 09:10:42 +05:00
Andrey Petelin
4897d3d292 refactor: Split nftables rules for TCP and UDP protocols separately 2025-09-14 08:55:34 +05:00
itdoginfo
0aa0a4a9c8 Add /usr/lib/podkop path 2025-09-13 19:23:09 +03:00
Andrey Petelin
7d082c5def chore: Update README task status from done to pending 2025-09-12 14:05:11 +05:00
Andrey Petelin
8845749517 chore: Update README.md to mark completed tasks 2025-09-12 14:03:32 +05:00
Andrey Petelin
054ed355cf fix: Add packet_encoding support for VLESS outbound configuration 2025-09-11 17:52:47 +05:00
Andrey Petelin
304c57edfa fix: Fix translation for "Local Subnet List Paths" 2025-09-11 17:04:22 +05:00
Andrey Petelin
8dd33cdde2 fix: Remove non-existent filename variable 2025-09-11 17:01:39 +05:00
Andrey Petelin
3d3fbe3bfb fix: Fix variable name in HTTP proxy check for wget command 2025-09-11 16:58:40 +05:00
Andrey Petelin
427ea3bc9a fix: Remove unused server_address variable from DNS configuration 2025-09-11 16:56:30 +05:00
Andrey Petelin
a7f6a993ac chore: shfmt formatting 2025-09-11 16:40:06 +05:00
Andrey Petelin
074c1a9349 chore: Remove TODO comments from user domains and subnets text input fields 2025-09-11 15:32:39 +05:00
Andrey Petelin
b6a6db71a8 refactor: Implement user domain and subnet list handling 2025-09-11 15:31:21 +05:00
Andrey Petelin
38fcb59ed7 fix: Reload config after commit to ensure runtime consistency 2025-09-11 13:37:49 +05:00
Andrey Petelin
5a2ffcfd38 refactor: Add graceful shutdown handling for dnsmasq reconfiguration 2025-09-11 11:43:58 +05:00
Andrey Petelin
49f12b212d chore: Update required sing-box version to 1.12.0 2025-09-10 19:44:31 +05:00
Andrey Petelin
489c61baa2 refactor: Move constants to constants.sh 2025-09-10 19:40:12 +05:00
Andrey Petelin
d4b5431db4 refactor: Refactor nft rules to use named sets for interfaces and localv4 2025-09-10 17:52:14 +05:00
Andrey Petelin
d0ea39abd0 refactor: Rename TEST_DOMAIN variable to FAKEIP_TEST_DOMAIN 2025-09-10 15:39:23 +05:00
Andrey Petelin
d4e754d2eb refactor: Rename FAKEIP constant to SB_FAKEIP_INET4_RANG 2025-09-10 14:14:15 +05:00
Andrey Petelin
82f9ae4c6a refactor: Remove redundant FakeIP config verification 2025-09-10 14:10:17 +05:00
Andrey Petelin
775b0073d3 refactor: Fixed nft rule for routing tagged traffic to localhost tproxy 2025-09-10 13:33:07 +05:00
Andrey Petelin
b477a8abc0 fix: Assign domain resolver tag correctly to DNS servers 2025-09-09 16:30:11 +05:00
Andrey Petelin
81e0c86060 refactor: Add block section support 2025-09-09 15:51:09 +05:00
Andrey Petelin
191522f396 chore: Rename download_to_tempfile to download_to_file for clarity 2025-09-09 11:48:03 +05:00
Andrey Petelin
79cea7a31a chore: remove .shellcheckrc file 2025-09-09 11:20:19 +05:00
Andrey Petelin
6c094aceae fix: Ensure unique values when patching local source ruleset 2025-09-09 11:15:29 +05:00
Andrey Petelin
1e8c2b50f7 chore: Rename NFT_GENERAL_SET_NAME to NFT_COMMON_SET_NAME 2025-09-08 23:10:53 +05:00
Andrey Petelin
27d2366208 chore: Remove list_update refactor TODO 2025-09-08 23:03:28 +05:00
Andrey Petelin
c1133827a2 fix: Pass HTTP proxy address to download functions for remote subnet imports 2025-09-08 23:00:26 +05:00
Andrey Petelin
a187192a88 chore: Move and rename _get_download_detour_tag to get_download_detour_tag 2025-09-08 22:55:42 +05:00
Andrey Petelin
fe30cf9e55 refactor: Refactoring list_update function 2025-09-08 22:43:27 +05:00
Andrey Petelin
9496a88774 refactor: Add ip addresses to nft set for local ruleset handling 2025-09-08 10:46:29 +05:00
Andrey Petelin
f54e92cd7a fix: Update config key from cache_file to cache_path in cache file configuration 2025-09-08 10:38:49 +05:00
Andrey Petelin
d70a04b144 refactor: Improve dnsmasq configuration logic for DNS handling 2025-09-07 18:11:46 +05:00
Andrey Petelin
e5be9c3fd1 refactor: Avoid unnecessary sing-box config writes by comparing hashes before saving (#128) 2025-09-07 12:45:27 +05:00
Andrey Petelin
9762b9cca4 refactor: Remove unused functions 2025-09-07 12:14:02 +05:00
Andrey Petelin
9d861cf3e0 feat: Add sing-box config path option (#128) 2025-09-07 12:12:08 +05:00
Andrey Petelin
49836e4adc feat: Add local subnet lists support with UI and backend integration (#156) 2025-09-05 21:23:55 +05:00
Andrey Petelin
5273935d25 chore: fix my perfect English 2025-09-05 17:01:36 +05:00
Andrey Petelin
d03167f49d chore: fix my perfect English 2025-09-05 16:49:05 +05:00
Andrey Petelin
da89c5c7df refactor: rename parameters for migration 2025-09-05 15:02:24 +05:00
Andrey Petelin
acfc95e86d refactor: Refactoring configuration of local domain lists 2025-09-05 14:50:43 +05:00
Andrey Petelin
17c1d09aa8 refactor: Enable cron job scheduling 2025-09-04 20:11:16 +05:00
Andrey Petelin
c7e21010bd refactor: Add download helper functions to helpers.sh and remove from main script 2025-09-04 20:09:44 +05:00
Andrey Petelin
f70e2ac557 refactor: Simplify cron job logic and renaming variable 2025-09-04 20:02:43 +05:00
Andrey Petelin
cb4e3036be chore: Remove module logging from log function 2025-09-04 18:00:28 +05:00
Andrey Petelin
12fc6bd9ac chore: undo renaming of taboption classarg 2025-09-04 16:43:39 +05:00
Andrey Petelin
2794cad533 fix: Remove direct outbound from DNS server configuration to prevent invalid detour 2025-09-04 12:35:53 +05:00
Andrey Petelin
9b182a3045 fix: fix detour parameter for remote ruleset 2025-09-04 12:28:38 +05:00
Andrey Petelin
f07d90a524 refactor: intermediate refactoring commit 2025-09-04 12:10:05 +05:00
Andrey Petelin
75fc377c22 Merge branch 'main' into refactoring 2025-09-04 11:52:52 +05:00
itdoginfo
33ecb771f9 Fix UDP over TCP for extra section 2025-09-02 14:44:14 +03:00
itdoginfo
86038e2756 Delay setting option 2025-09-02 14:09:35 +03:00
Andrey Petelin
db91c628c8 Merge remote-tracking branch 'origin/refactoring' into refactoring
# Conflicts:
#	podkop/files/usr/lib/sing_box_config_manager.sh
2025-08-31 20:25:16 +05:00
Andrey Petelin
41ce41945c feat: Add domain_resolver and detour parameters to DNS server configurations in sing-box manager script 2025-08-31 20:22:57 +05:00
Andrey Petelin
2753a44440 feat: Add domain_resolver parameter to DNS server configurations in sing-box manager script 2025-08-31 19:43:21 +05:00
Andrey Petelin
cd1a4e2a8e chore: update example links with valid values 2025-08-31 13:19:04 +05:00
Andrey Petelin
7e041da8c6 feat: add sing-box configuration manager script and jq helpers 2025-08-31 13:09:55 +05:00
itdoginfo
f3f5bca555 Add HODCA, CloudFront, DO 2025-08-29 15:02:43 +03:00
itdoginfo
174f16bc76 Update 2025-08-28 19:11:37 +03:00
itdoginfo
7c63a35faa Old value #131 2025-08-28 18:40:53 +03:00
itdoginfo
86a86df982 Fix template 2025-08-28 18:12:35 +03:00
itdoginfo
ac445bc227 Fix template config 2025-08-28 18:08:10 +03:00
itdoginfo
4398e6885b Add template for issues and PR 2025-08-28 18:06:08 +03:00
itdoginfo
9974b42cc2 NFT: output chain for traffic from the router 2025-08-27 00:31:56 +03:00
itdoginfo
8cd990f8a3 Fix extension for list 2025-08-26 14:25:28 +03:00
itdoginfo
c509fd38c7 Fix version for docker 2025-08-26 14:16:08 +03:00
itdoginfo
38991a803a Fix 2025-08-25 19:44:16 +03:00
itdoginfo
29c34e31db Fix 2025-08-25 19:41:04 +03:00
itdoginfo
a77e8fae7d Disable tag check 2025-08-25 19:13:01 +03:00
itdoginfo
6d83737336 Auto version for make 2025-08-25 19:11:12 +03:00
itdoginfo
84115e2f3b v0.4.7 2025-08-25 17:02:46 +03:00
itdoginfo
2dbdb9d2c1 Global check: WG Route 2025-08-24 16:13:41 +03:00
itdoginfo
88c6717152 Disable delay 2025-08-24 13:44:49 +03:00
itdoginfo
b3986308ce Cut WRP prefix 2025-08-24 13:44:39 +03:00
itdoginfo
a15c3cf171 Update #147 2025-08-24 11:59:40 +03:00
itdoginfo
4c91223f85 Merge pull request #136 from SaltyMonkey:main
User Subnet validation for glob ip, init.d/zapret proper check, passwall in conflicts
2025-08-24 11:05:55 +03:00
itdoginfo
7cf7b1f626 Merge branch 'main' into main 2025-08-24 11:04:39 +03:00
SaltyMonkey
a2536534f8 Remove opkg list-installed checks for packages from conflicts 2025-08-24 10:00:13 +03:00
itdoginfo
c49354fe38 Merge pull request #149 from ampetelin:json_srs_lists
Added support for JSON and SRS
2025-08-23 18:53:37 +03:00
Andrey Petelin
6e01e036eb handle missing ip_cidr in rulesets 2025-08-23 20:42:43 +05:00
Andrey Petelin
7484d0c203 Fixed logging of custom ruleset preparation 2025-08-23 19:36:34 +05:00
Andrey Petelin
0eb4ca4ea9 Removed filepath from wget when downloading via proxy 2025-08-23 19:33:47 +05:00
Andrey Petelin
c2d95162b7 Added support for JSON and SRS rulesets 2025-08-20 21:42:46 +05:00
SaltyMonkey
1fc2947fbc Log messages for nextdns 2025-07-25 09:41:58 +03:00
SaltyMonkey
ea931d8463 added nextdns package in conflicts 2025-07-25 09:38:48 +03:00
SaltyMonkey
e2f36c35d4 Log messages for luci-app-passwall and luci-app-passwall in binary 2025-07-12 01:08:39 +03:00
SaltyMonkey
e8f8dcc5e7 added passwall and passwall2(paid version?) in conflicts 2025-07-12 01:00:00 +03:00
SaltyMonkey
1e2174bb80 User Subnets validation: 0.0.0.0 is not allowed 2025-07-12 00:40:51 +03:00
SaltyMonkey
85e515ef15 Fix /etc/init.d/zapret check in global_check, cleanup useless whitespaces 2025-07-12 00:29:33 +03:00
itdoginfo
418cdc4366 rm iptables-mod-extra check 2025-06-30 16:56:39 +03:00
itdoginfo
25b0dcaad5 v0.4.6 2025-06-30 16:27:44 +03:00
itdoginfo
cc59e756dd br_netfilter. Cache size unset. Mixed & source_ip_cidr 2025-06-30 16:26:31 +03:00
itdoginfo
210714c499 unnecessary check 2025-06-30 16:24:33 +03:00
itdoginfo
8b6c336584 Change to 1.1.1.1 2025-06-27 23:54:52 +03:00
itdoginfo
5c543c1608 Change to procd_add_interface_trigger. Added PROCD_RELOAD_DELAY 2025-06-27 23:54:31 +03:00
itdoginfo
ac274d8796 v0.4.5 2025-06-25 23:38:55 +03:00
itdoginfo
ce1f86ceb7 Added split dns. Func for build sing-box config 2025-06-25 23:34:39 +03:00
itdoginfo
1fd67eefb3 Fix eng phrase 2025-06-25 23:32:33 +03:00
itdoginfo
e7b726d27c Merge pull request #127 from procudin/fix/extra-configs-visibility
fix: hide extra configs for non-basic tabs
2025-06-23 13:58:25 +03:00
Artem Prokudin
adb16e7f74 fix: hide extra configs for non-basic tabs 2025-06-15 10:57:16 +03:00
itdoginfo
51da8c22fd Update 2025-06-03 15:47:13 +03:00
itdoginfo
41351dafd2 Removed the installation awg/wg/ovpn/oc. Refactoring 2025-06-03 15:45:27 +03:00
itdoginfo
2aee77b9a2 v0.4.4 Added independent_cache 2025-06-02 15:40:14 +03:00
itdoginfo
2a1a220dc8 v0.4.3 2025-05-22 12:07:08 +03:00
itdoginfo
608caba090 Merge pull request #115 from itdoginfo/fix/comments
♻️ refactor(podkop): update command scheduling and priority handling
2025-05-22 12:01:53 +03:00
Ivan K
04af8c9649 ♻️ refactor(podkop): update command scheduling and priority handling 2025-05-22 11:09:02 +03:00
itdoginfo
88d108e5ab Fix i18n version 2025-05-21 19:43:27 +03:00
itdoginfo
8ce6790355 v0.4.2 2025-05-21 19:18:07 +03:00
itdoginfo
8e7b40cf56 Ready image 2025-05-21 19:17:55 +03:00
itdoginfo
21fa017443 Merge pull request #114 from itdoginfo/fix/comments
Fix/comments
2025-05-21 15:43:58 +03:00
Ivan K
f1954df83b ♻️ refactor(diagnosticTab): move command execution helpers to utils.js 2025-05-21 15:09:42 +03:00
Ivan K
8573bd99b5 ♻️ refactor(diagnosticTab): remove unused debug 2025-05-21 14:21:37 +03:00
Ivan K
c3f44bd124 fix(diagnosticTab): add error polling and notification system 2025-05-21 14:18:23 +03:00
itdoginfo
59e394c4f2 v0.4.1 JS refactoring 2025-05-21 14:16:16 +03:00
itdoginfo
c897c90371 Merge pull request #110 from itdoginfo/fix/comments
♻️ refactor(podkop): modularize configuration and diagnostics sections
2025-05-21 13:48:30 +03:00
Ivan K
bcab66f88c ♻️ refactor(podkop): enhance check_nft function for domain-specific set statistics 2025-05-21 09:48:53 +03:00
Ivan K
05a551e5e3 💄 style(podkop): remove extra newline in NFT check completed message 2025-05-20 19:28:40 +03:00
Ivan K
1f81ec8403 🔧 chore(podkop): use check_nft in global_check 2025-05-20 17:32:04 +03:00
Ivan K
9748178562 ♻️ refactor(podkop): enhance nft set statistics and chain configurations 2025-05-20 17:28:00 +03:00
Ivan K
1411e7d403 ♻️ refactor(diagnosticTab): improve command execution and UI updates 2025-05-19 20:32:08 +03:00
Ivan K
d81a90bd28 ♻️ refactor(diagnosticTab): improve status updates and caching 2025-05-19 19:59:29 +03:00
Ivan K
82f4720326 ♻️ refactor(podkop): rename sections into correct files 2025-05-18 15:58:59 +03:00
Ivan K
10f246ea61 ♻️ refactor(podkop): move URL validation to config.js 2025-05-16 23:30:23 +03:00
Ivan K
c0571320f1 ♻️ refactor(networkUtils): remove custom network functions 2025-05-16 22:22:07 +03:00
Ivan K
a658ca5518 💄 style(podkop): remove unused networkUtils import 2025-05-16 18:40:29 +03:00
Ivan K
08709c93c7 ♻️ refactor(podkop): rename section variables for clarity 2025-05-16 18:28:06 +03:00
Ivan K
cf5b2216be ♻️ refactor(podkop): reorganize sections into subdirectory 2025-05-16 18:23:16 +03:00
Ivan K
682913ade0 ♻️ refactor(podkop): remove unused parameter from createAdditionalSection 2025-05-16 18:08:29 +03:00
Ivan K
3b2cbd0332 ♻️ refactor(podkop): modularize configuration and diagnostics sections 2025-05-16 18:04:33 +03:00
itdoginfo
8f9dcf2c55 Merge pull request #109 from itdoginfo/fix/comments
♻️ refactor(podkop): refactor domain list and API endpoints
2025-05-16 14:50:59 +03:00
Ivan K
91d027b5fe ♻️ refactor(podkop): refactor domain list and API endpoints 2025-05-16 14:46:06 +03:00
itdoginfo
f90ab7f468 v0.4.0 beta 2025-05-15 13:26:57 +03:00
itdoginfo
e4bfd447ce Merge pull request #108 from itdoginfo/fix/comments
fix(podkop): add dont touch my dhcp logic to fake IP check functions
2025-05-15 12:07:08 +03:00
Ivan K
fbdd759b83 ♻️ refactor(podkop): remove redundant DNS/sing-box checks in fakeip status 2025-05-15 11:53:30 +03:00
Ivan K
2488bc30b1 fix(podkop): add dont touch my dhcp logic to fake IP check functions 2025-05-15 11:31:33 +03:00
itdoginfo
dcc12cf920 Merge pull request #107 from itdoginfo/fix/comments
💄 style(podkop): update modal button titles and clipboard content
2025-05-14 14:33:53 +03:00
Ivan K
c99cef9f27 💄 style(podkop): update modal button titles and clipboard content 2025-05-14 14:30:52 +03:00
itdoginfo
8a68f3fcc2 Update 2025-05-13 14:26:50 +03:00
itdoginfo
ed2994be3a v0.3.50 Adde Google AI, Play, HTZ, OVH of list 2025-05-12 23:31:06 +03:00
itdoginfo
77ff5ab781 Merge pull request #105 from itdoginfo/fix/comments
️ perf(dns): improve DNS query performance and error handling
2025-05-12 18:17:50 +03:00
Ivan K
1c80bc5a5e ️ perf(dns): improve DNS query performance and error handling 2025-05-12 17:50:50 +03:00
itdoginfo
f688d74c32 v0.3.49 Improved diagnostics 2025-05-12 17:32:51 +03:00
itdoginfo
7bc50d58d3 Merge pull request #104 from itdoginfo/fix/comments
Fix/comments
2025-05-12 17:25:59 +03:00
Ivan K
77ce0c380b 🐛 fix(dns): improve DNS availability check logic 2025-05-12 17:21:38 +03:00
Ivan K
47d1b349c7 ♻️ refactor(podkop): add local var 2025-05-12 16:51:59 +03:00
Ivan K
e9face1f4a ♻️ refactor(podkop): simplify logging function 2025-05-12 16:48:59 +03:00
itdoginfo
e5bf7d9bed Merge pull request #103 from itdoginfo/fix/comments
♻️ refactor(podkop): improve diagnostics and error handling
2025-05-12 16:48:15 +03:00
Ivan K
dd4722f3e1 ♻️ refactor(podkop): simplify logging functions 2025-05-12 16:40:44 +03:00
Ivan K
1e945dafe7 feat(logging): add colored logging to stdout and syslog 2025-05-12 15:54:12 +03:00
itdoginfo
b080521a58 Update 2025-05-12 00:57:52 +03:00
itdoginfo
6a96a85773 Update 2025-05-12 00:49:19 +03:00
itdoginfo
6fb3a36974 Update 2025-05-12 00:46:30 +03:00
Ivan K
b3dbee1dbe 💄 style(podkop): adjust margin styles in status panel 2025-05-11 20:30:16 +03:00
Ivan K
916321578d ️ feat(dns): add random DNS query ID generation 2025-05-11 19:33:08 +03:00
Ivan K
c74d733717 ♻️ refactor(podkop): improve diagnostics and error handling 2025-05-11 19:26:29 +03:00
itdoginfo
433724f762 v0.3.48 Custom URL CRLF 2025-05-10 18:55:35 +03:00
itdoginfo
6378aa9910 Update 2025-05-10 16:15:53 +03:00
itdoginfo
68f5f123ca v0.3.47 Fix noresolv 1 2025-05-10 12:50:01 +03:00
itdoginfo
fae43d0471 v0.3.46 2025-05-08 19:24:08 +03:00
itdoginfo
9d6dc45fdb #99 Block mode 2025-05-08 19:23:45 +03:00
itdoginfo
9aa5a2d242 Fix site. #100 added ntpd 2025-05-08 10:14:58 +03:00
itdoginfo
63dc86fca4 v0.3.45 Update checker domain 2025-05-07 22:39:23 +03:00
itdoginfo
4d9cedaf4c Return upgrade command 2025-05-07 17:58:19 +03:00
itdoginfo
14e7cbae01 v0.3.44 2025-05-07 17:26:57 +03:00
itdoginfo
c9f610bb1e Change to ip.podkop.net. Fix log. Added restart 2025-05-07 17:24:32 +03:00
itdoginfo
19671c7f67 Stop btn. Change to ip.podkop.net 2025-05-07 17:23:20 +03:00
itdoginfo
6d1e4091e5 Detour 2025-05-07 00:22:04 +03:00
itdoginfo
96d661c49f Fixed default values ttl in comment 2025-05-03 18:52:57 +03:00
itdoginfo
da8dd06b34 Move doc to wiki 2025-05-03 18:12:00 +03:00
itdoginfo
2c1bcffb6d fix iptables 2025-05-03 18:11:49 +03:00
itdoginfo
3040ce7286 v0.3.43 2025-05-02 14:53:11 +03:00
itdoginfo
e025271a14 Added to global check: DNS check and proxy check. From VizzleTF 2025-05-02 14:50:06 +03:00
itdoginfo
2b8208186d Fix global check text 2025-05-02 13:55:07 +03:00
itdoginfo
17fb11baf0 Fixed diagnostics from VizzleTF 2025-05-02 13:34:11 +03:00
itdoginfo
3c1b041b52 Edited text from #96 2025-05-01 22:52:41 +03:00
itdoginfo
38acac1a31 Merge pull request #96 from itdoginfo/chore/sing-box-status
Issue #91 , Issue #94
2025-05-01 22:16:38 +03:00
Ivan K
2939229df3 back to the future 2025-05-01 19:30:05 +03:00
Ivan K
26c3d0bc7e ♻️ refactor(podkop): simplify DoH URL determination logic 2025-05-01 19:26:21 +03:00
Ivan K
b364363b1b feat(dns): add DoH URL resolution function 2025-05-01 19:20:36 +03:00
itdoginfo
d85caf0c0c Fix https-dns-proxy i18 2025-05-01 19:10:18 +03:00
Ivan K
65f72e1e04 ♻️ refactor(podkop): update WARP detection logic 2025-05-01 18:29:42 +03:00
Ivan K
e59ef6dd6f 💄 style(podkop): remove unnecessary sed commands in global_check 2025-05-01 17:57:51 +03:00
Ivan K
05272de650 💄 style(podkop): update formatting and messages 2025-05-01 17:48:25 +03:00
Ivan K
48716e7156 Enhance Podkop functionality with global check feature and improved diagnostics. Added support for FakeIP tests in both browser and router contexts. Updated UI elements for better status reporting and added localization for new messages. 2025-05-01 17:18:07 +03:00
itdoginfo
f29b97e495 v0.3.42 2025-05-01 14:17:18 +03:00
itdoginfo
41c21cebcd Fixed validation for ws 2025-04-30 23:43:36 +03:00
itdoginfo
238e99a547 Update 2025-04-30 19:02:31 +03:00
itdoginfo
4f44fcfe99 Update 2025-04-30 14:48:12 +03:00
itdoginfo
9fd2fb9b6e Update 2025-04-30 00:19:42 +03:00
itdoginfo
c0591b25b9 Fix 2025-04-30 00:16:09 +03:00
itdoginfo
97fd392334 Fixed read. Added upgrade flag 2025-04-30 00:11:55 +03:00
itdoginfo
848c784cc0 Fix 2025-04-29 23:49:28 +03:00
itdoginfo
ab971dcd36 Update 2025-04-29 23:48:49 +03:00
itdoginfo
b8d96f28cd Added CF. Fixed https-dns-proxy warning. Masked for static wan 2025-04-29 18:54:50 +03:00
itdoginfo
f2268fd494 v0.3.41. Improved Diagnotics: WAN, WARP, versions, etc 2025-04-29 12:53:29 +03:00
itdoginfo
19897afcdd v0.3.40. Improved Diagnotics 2025-04-28 00:33:07 +03:00
itdoginfo
0e2ea60f01 v0.3.39. Added global check button 2025-04-27 19:29:34 +03:00
itdoginfo
2dc5944961 Fix https-dns-proxy --force-depends 2025-04-27 18:07:58 +03:00
itdoginfo
f65de36804 Detect https-dns-proxy 2025-04-27 15:50:37 +03:00
itdoginfo
19541f8bb3 v0.3.38. fix reload config luci 2025-04-26 22:35:11 +03:00
itdoginfo
aa42c707fe v0.3.37 2025-04-26 17:49:28 +03:00
itdoginfo
bf96f93987 Fix kill stderr. Return if 127.0.0.42 exists 2025-04-26 17:49:04 +03:00
itdoginfo
ff9aad8947 Option enable iface mon 2025-04-26 17:47:52 +03:00
itdoginfo
d9718617bd Option enable iface mon 2025-04-26 17:47:42 +03:00
itdoginfo
e865c9f324 Validate raw network. Path for DoH. Bool for iface monitoring 2025-04-26 17:47:08 +03:00
itdoginfo
7df8bb5826 rmempty proxy url string 2025-04-25 19:29:31 +03:00
itdoginfo
f960358eb6 0.3.36 2025-04-25 10:57:59 +03:00
itdoginfo
ba44966c02 Interface trigger. Disable sing-box autostart. dont touch dhcp. reload without dnsmasq restart 2025-04-24 19:25:08 +03:00
itdoginfo
615241aa37 Merge pull request #88 from Davoyan/patch-1
Update localisation
2025-04-22 11:36:38 +03:00
Davoyan
9a3220d226 Update localisation 2025-04-22 11:24:54 +03:00
itdoginfo
ec8d28857e #82 and #83 2025-04-15 00:42:16 +03:00
itdoginfo
26b49f5bbb Check fix 2025-04-15 00:15:28 +03:00
itdoginfo
0a7efb3169 Fix 2025-04-03 17:53:21 +03:00
itdoginfo
468e51ee8e v0.3.35 2025-04-03 17:42:45 +03:00
itdoginfo
3b93a914de v0.3.34 2025-04-03 17:27:35 +03:00
itdoginfo
76c5baf1e2 Fix tailscale smartdns in resolve.conf 2025-04-03 17:27:13 +03:00
itdoginfo
c752c46abf Fix resolv_conf value 2025-04-03 17:24:57 +03:00
itdoginfo
1df1defa5e Check curl 2025-04-03 17:24:31 +03:00
itdoginfo
3cb4be6427 v0.3.33 2025-04-03 16:47:47 +03:00
itdoginfo
25bfdce5ce Added critical log. Rm friendlywrt check. Added iptables check 2025-04-03 16:47:26 +03:00
itdoginfo
6d0f097a07 Merge pull request #75 from itdoginfo/feature/error-notification (#47)
Feature/error notification
2025-04-03 14:52:00 +03:00
Ivan K
5f780955eb 💄 style(podkop): update error log filtering criteria 2025-04-03 13:42:55 +03:00
Ivan K
389def9056 ♻️ refactor(podkop): remove unused createErrorModal function 2025-04-03 13:40:41 +03:00
Ivan K
e816da5133 feat(podkop): add error logging and notification system 2025-04-03 13:36:22 +03:00
Ivan K
e57adbe042 🔒 refactor(config): Mask NextDNS server address in config output 2025-03-30 20:36:49 +03:00
itdoginfo
d78c51360d Merge pull request #73 from itdoginfo/feature/no-more-cache
🐛 fix(doh): Improve DoH server compatibility detection for quad9
2025-03-30 18:44:26 +03:00
Ivan K
c2357337fc 🐛 fix(dns): improve DoH server compatibility and error handling 2025-03-30 17:20:06 +03:00
Ivan K
bc6490b56e 🐛 fix(doh): Improve DoH server compatibility detection for quad9 2025-03-30 17:04:30 +03:00
itdoginfo
2f645d9151 v0.3.32 2025-03-30 16:03:49 +03:00
itdoginfo
94cc65001b Merge pull request #72 from itdoginfo/feature/no-more-cache
🐛 fix(podkop): Handle DNS check errors and timeouts properly
2025-03-30 16:02:44 +03:00
Ivan K
87caa70e97 feat(dns): Mask NextDNS ID in DNS availability check output 2025-03-30 14:53:01 +03:00
Ivan K
90d7c60fcb 🐛 fix(podkop): Handle DNS check errors and timeouts properly 2025-03-30 14:46:03 +03:00
itdoginfo
3f114b4710 v0.3.31 2025-03-30 12:41:40 +03:00
itdoginfo
b821abe82c Merge pull request #67 from itdoginfo/feature/no-more-cache
Feature/add DNS and bypass status checks to diagnostics
2025-03-30 12:37:58 +03:00
Ivan K
732cab2ef3 🐛 fix(podkop): fix typo in translation and dns check timeout 2025-03-30 09:54:31 +03:00
Ivan K
3b4ce9e7a3 feat: add visibility change event listener for diagnostics updates 2025-03-21 15:06:08 +03:00
Ivan K
69c4445c85 refactor: move buttons for NFT and DNSMasq checks 2025-03-21 14:54:52 +03:00
Ivan K
dcebc3d67d docs: update Russian translations for proxy configuration and add new translation strings 2025-03-21 14:53:24 +03:00
Ivan K
1be31eaf59 feat: add local DNS availability check and display in UI 2025-03-21 14:47:20 +03:00
Ivan K
023210e0f0 style: update text for Bypass Status to Main config 2025-03-21 14:21:35 +03:00
Ivan K
5ff832533e feat: add DNS and bypass status checks to diagnostics 2025-03-21 13:03:29 +03:00
Ivan K
5d2163515e refactor: improve caching prevention logic 2025-03-20 21:47:55 +03:00
Ivan K
5865706d0c feat: add timestamp to URL to prevent caching 2025-03-20 21:45:08 +03:00
itdoginfo
aabe1c53dc Update 2025-03-18 00:32:06 +03:00
itdoginfo
8e91b582ad #36 2025-03-18 00:31:56 +03:00
itdoginfo
62ce1f5acc v0.3.30 2025-03-17 14:44:07 +03:00
itdoginfo
93727ddeb5 Processing empty values 2025-03-17 14:43:33 +03:00
itdoginfo
98797d93b1 v0.3.29 2025-03-17 13:16:38 +03:00
itdoginfo
66c6e998a2 #38 #46 2025-03-17 13:14:37 +03:00
itdoginfo
3d9f82b571 Merge pull request #65 from itdoginfo/chore/fakeip-method
feat: add diagnostics functionality only in tab
2025-03-14 12:33:52 +03:00
Ivan K
38d082e236 feat: add diagnostics functionality only in tab 2025-03-14 09:50:28 +03:00
itdoginfo
9f5abcae6d v0.3.28 2025-03-13 19:34:52 +03:00
itdoginfo
7836d2c6ec Fix 2025-03-13 19:32:57 +03:00
itdoginfo
f46c934c59 Test 2025-03-13 19:30:17 +03:00
itdoginfo
23ed10d393 Added check version in Makefile 2025-03-13 19:28:31 +03:00
itdoginfo
26488baad3 Merge pull request #64 from itdoginfo/chore/fakeip-method
fix: fix enable/disable functionality to podkop service
2025-03-13 19:05:49 +03:00
Ivan K
c79016e456 feat: add createInitActionButton function to ButtonFactory 2025-03-13 10:35:48 +03:00
Ivan K
884bbfee42 fix: remove unused button creation code 2025-03-13 10:32:34 +03:00
Ivan K
1263b9b1b8 fix: fix enable/disable functionality to podkop service 2025-03-13 10:00:37 +03:00
Ivan K
23203fd7a1 feat: add createSystemButton function to ButtonFactory and flush cache button 2025-03-13 00:10:18 +03:00
itdoginfo
25c887a952 v0.3.27 2025-03-12 17:20:35 +03:00
itdoginfo
e7a3c7adf1 Merge pull request #63 from itdoginfo/chore/fakeip-method
feat: update DNS checks and improve FakeIP status reporting
2025-03-12 17:02:18 +03:00
Ivan K
3e96b9a1af feat: update DNS checks and improve FakeIP status reporting 2025-03-12 16:20:59 +03:00
itdoginfo
251f94cb88 v0.3.26 2025-03-12 14:59:03 +03:00
itdoginfo
44936c698e Merge pull request #62 from itdoginfo/chore/fakeip-method
feat: add CLI check for FakeIP functionality and update status display
2025-03-12 14:57:41 +03:00
Ivan K
0faaca12fc сhore: remove tabs 2025-03-12 14:54:56 +03:00
Ivan K
c6d1f05916 feat: add CLI check for FakeIP functionality and update status display 2025-03-11 19:14:21 +03:00
itdoginfo
57554d518b v0.3.25 2025-03-11 18:39:30 +03:00
itdoginfo
09d761956c Some fixes 2025-03-11 18:39:18 +03:00
itdoginfo
ada807fec3 v0.3.24 2025-03-07 14:46:45 +03:00
itdoginfo
b28a5f1293 New default TTL=60, DOH=8.8.8.8 2025-03-07 14:46:22 +03:00
itdoginfo
2332eae5ff Added dns and github checker. JSON file for custom URL lists 2025-03-07 14:45:36 +03:00
itdoginfo
a755b6661d Merge pull request #59 from itdoginfo/feat/multiple-mixed-inbounds
Add support for multiple mixed inbounds with unique ports
2025-03-07 13:10:32 +03:00
Nikita Skryabin
567ce52253 feat: add support for multiple mixed inbounds with unique ports 2025-03-06 22:54:25 +03:00
Nikita Skryabin
b736360b66 fix: ensure routing rule for mixed-in is always applied 2025-03-06 21:55:40 +03:00
itdoginfo
3b2a7ba8af Create /usr/bin/podkop 2025-03-05 01:08:30 +03:00
itdoginfo
c96de62d96 v0.3.22 2025-03-04 13:36:43 +03:00
itdoginfo
14b7fbe4f7 Fix cidr for all_traffic+exclude 2025-03-04 13:36:20 +03:00
itdoginfo
3d05fe8be4 0.3.21 2025-03-03 21:28:21 +03:00
itdoginfo
6ddf9d3b24 Fix section for all_traffic_ip 2025-03-03 21:28:12 +03:00
itdoginfo
b401243f74 0.3.20 2025-03-03 18:26:19 +03:00
itdoginfo
407ef404ac Fix ip_cidr+fakeip, all_traffic_from_ip_enabled list 2025-03-03 18:26:02 +03:00
itdoginfo
f2e45bbbb9 Fix default value 2025-03-03 11:21:49 +03:00
itdoginfo
c2b37a14f4 v0.3.19 2025-02-26 18:24:40 +03:00
itdoginfo
3d029edaea Update 2025-02-26 18:23:02 +03:00
itdoginfo
b86d6d6294 Merge pull request #52 from itdoginfo/fix/increase-timeout-safeexec
feat: add support for comments in proxy and domain/subnet configuration
2025-02-26 18:18:43 +03:00
Ivan K
5c48ead9e4 feat: add support for comments in proxy and domain/subnet configuration 2025-02-24 23:02:23 +03:00
Ivan K
53475b5e8a fix: increase timeout for safeExec function 2025-02-24 20:07:47 +03:00
Ivan K
59e1d75870 refactor: increase timeout for safeExec function 2025-02-24 19:37:59 +03:00
itdoginfo
3ec6cc4d84 0.3.18 2025-02-24 18:07:15 +03:00
itdoginfo
3413af9f94 Merge pull request #51 from itdoginfo/fix/vpn-devices
feat: add section_id parameter to getNetworkInterfaces function
2025-02-24 17:42:30 +03:00
Ivan K
76b5ceae5c feat: add section_id parameter to getNetworkInterfaces function 2025-02-24 17:39:56 +03:00
itdoginfo
99ccd9fbb3 0.3.17 2025-02-24 16:42:35 +03:00
itdoginfo
b82c6eb718 Merge pull request #50 from itdoginfo/fix/many-sni-support
feat: update network interface loading in podkop.js
2025-02-24 16:24:53 +03:00
Ivan K
ccc87d9aa0 feat: update network interface loading in podkop.js 2025-02-24 16:23:05 +03:00
183 changed files with 27091 additions and 4996 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

74
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
---
name: 🐛 Сообщение об ошибке
description: Создавайте только, если проблема точно не на вашей стороне.
title: "[BUG] "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: |
Спасибо за создание отчета об ошибке!
Перед отправкой, пожалуйста:
- Проверьте [существующие issues](https://github.com/itdoginfo/podkop/issues)
- Просмотрите [документацию](https://podkop.net)
- type: textarea
id: description
attributes:
label: 📝 Описание проблемы
description: Четкое и краткое описание того, что не работает
placeholder: Опишите проблему
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Шаги для воспроизведения
description: Шаги для воспроизведения проблемы. Если вы настраваете что-то по мануалу, приложите ссылку на него.
placeholder: |
1.
2.
3.
4.
validations:
required: true
- type: textarea
id: expected
attributes:
label: ✅ Ожидаемое поведение
description: Четкое и краткое описание того, что должно было произойти
placeholder: Опишите ожидаемое поведение
validations:
required: true
- type: textarea
id: environment
attributes:
label: 🖥️ Информация о системе
description: |
Информация о вашей системе (заполните всё применимое)
value: |
- **OpenWrt версия**:
- **Podkop версия**:
- **Роутер модель**:
- **Sing-box версия**:
render: markdown
validations:
required: true
- type: textarea
id: config
attributes:
label: ⚙️ Конфигурация
description: |
Релевантные части конфигурации (удалите чувствительную информацию!)
placeholder: |
Например:
- Содержимое /etc/config/podkop
- Конфигурация sing-box (если релевантно)
- Дополнительные конфиги, которые потребуются wireless/network/dhcp и т.д.
render: shell

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Если у вас что-то не работает, прежде всего прочитайте README проекта
url: https://github.com/itdoginfo/podkop
about: README проекта
- name: 📚 Если вы не нашли в README документацию, то вот ссылка на неё
url: https://podkop.net
about: Официальная документация PodKop

View File

@@ -0,0 +1,68 @@
---
name: ✨ Запрос новой функции
description: Предложите новую функцию или улучшение для Podkop
title: "[FEATURE] "
labels: ["enhancement", "needs-discussion"]
assignees: []
body:
- type: markdown
attributes:
value: |
Спасибо за предложение новой функции!
Перед отправкой, пожалуйста:
- Проверьте [существующие запросы](https://github.com/itdoginfo/podkop/issues?q=is%3Aissue+label%3Aenhancement)
- Убедитесь, что функции не существует в [документации](https://podkop.net)
- type: textarea
id: summary
attributes:
label: Краткое описание
description: Краткое описание предлагаемой функции
placeholder: В одном предложении опишите, что вы хотите добавить...
validations:
required: true
- type: textarea
id: problem
attributes:
label: Проблема, которую решает
description: |
Описание проблемы или неудобства, которое решит эта функция
placeholder: |
Сейчас нет возможности [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: 💡 Предлагаемое решение
description: Четкое и краткое описание того, что вы хотите реализовать
placeholder: |
Я хочу, чтобы Podkop мог [...]
Предлагаю добавить функцию, которая [...]
Можно было бы улучшить [...] путем [...]
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Workaround
description: |
Опишите альтернативные решения или функции, которые вы рассматривали
Есть ли обходные пути, которые вы используете сейчас?
placeholder: |
Сейчас я решаю это проблему путем [...]
Альтернативой могло бы быть [...]
Пробовал использовать [...], но это не подходит потому что [...]
- type: textarea
id: implementation
attributes:
label: Идеи реализации (опционально)
description: |
Если у вас есть идеи о том, как это можно реализовать, поделитесь ими. Помните про ограничения LuCI.
placeholder: |
Это можно реализовать с помощью [...]

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
# Описание изменений
Краткое описание ваших изменений и их цель.
## Что изменено
Детальное описание изменений:
-
-
-
(Этим вы экономите время ревьювера)

View File

@@ -2,43 +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
- name: Build and push
uses: docker/build-push-action@v6.9.0
- 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: 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: |
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 }}

78
.github/workflows/frontend-ci.yml vendored Normal file
View 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 "![Success](https://cdn2.combot.org/boratbrat/webp/6xf09f988f.webp)" >> $GITHUB_STEP_SUMMARY

49
.github/workflows/shellcheck.yml vendored Normal file
View 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 }}

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.idea
fe-app-podkop/node_modules
fe-app-podkop/.env
.DS_Store

View File

@@ -1,8 +0,0 @@
FROM openwrt/sdk:x86_64-v23.05.5
RUN ./scripts/feeds update -a && ./scripts/feeds install luci-base && mkdir -p /builder/package/feeds/utilites/ && mkdir -p /builder/package/feeds/luci/
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

11
Dockerfile-apk Normal file
View File

@@ -0,0 +1,11 @@
FROM itdoginfo/openwrt-sdk-apk:09102025
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 -j1 V=s && \
make package/luci-app-podkop/compile -j1 V=s

11
Dockerfile-ipk Normal file
View File

@@ -0,0 +1,11 @@
FROM itdoginfo/openwrt-sdk-ipk:24.10.3
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

187
README.md
View File

@@ -1,180 +1,61 @@
# Вещи, которые вам нужно знать перед установкой
- Это альфа версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
- Основной функционал работает, но побочные штуки сейчас могут сбоить.
- При обновлении **обязатально** сбрасывайте кэш LuCI.
- Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
- При возникновении проблем, нужен технически грамотный фидбэк в чат. Ознакомьтесь с закрепом в топике.
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
- Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
- Необходимо минимум 15МБ свободного места на роутере. Роутерами с флешками на 16МБ сразу мимо.
- Необходимо минимум 25МБ свободного места на роутере. Роутеры с флешками на 16МБ сразу мимо.
- При старте программы редактируется конфиг Dnsmasq.
- Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен.
- Информация здесь может быть устаревшей. Все изменения фиксируются в телеграм-чате https://t.me/itdogchat - топик **Podkop**.
- Если у вас не что-то не работает, то следуюет сходить в телеграм чат, прочитать закрепы и выполнить что там написано..
- Если у вас установлен Getdomains, его следует удалить.
- Информация здесь может быть устаревшей. Все изменения фиксируются в [телеграм-чате](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.
- Dashboard доступен, если вы заходите по http (из-за особенностей clash api). И не будет работать, если вы заходите по https и/или домену.
# Удаление GetDomains скриптом
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/domain-routing-openwrt/refs/heads/master/getdomains-uninstall.sh)
```
Оставляет туннели, зоны, forwarding. А также stubby и dnscrypt. Они не помешают. Конфиг sing-box будет перезаписан в podkop.
# Документация
https://podkop.net/
# Установка Podkop
Пакет работает на всех архитектурах.
Тестировался на **ванильной** OpenWrt 23.05 и OpenWrt 24.10.
На FriendlyWrt 23.05 присуствуют зависимости от iptables, которые ломают tproxy. Если у вас появляется warning про это в логах, следуйте инструкции по приведённой там ссылке.
Полная информация в [документации](https://podkop.net/docs/install/)
Поддержки APK на данный момент нет. APK будет сделан после того как разгребу основное.
## Автоматическая
Вкратце, достаточно одного скрипта для установки и обновления:
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
```
Скрипт также предложит выбрать, какой туннель будет использоваться. Для выбранного туннеля будут установлены нужные пакеты, а для Wireguard и AmneziaWG также будет предложена автоматическая настройка - прямо в консоли скрипт запросит данные конфига. Для AmneziaWG можно также выбрать вариант с использованием конфига обычного Wireguard и автоматической обфускацией до AmneziaWG.
## Изменения 0.7.0
Начиная с версии 0.7.0 изменена структура конфига `/etc/config/podkop`. Старые значения несовместимы с новыми. Нужно заново настроить Podkop.
Для AmneziaWG скрипт проверяет наличие пакетов под вашу платформу в [стороннем репозитории](https://github.com/Slava-Shchipunov/awg-openwrt/releases), так как в официальном репозитории OpenWRT они отсутствуют, и автоматически их устанавливает.
Скрипт установки обнаружит старую версию и предупредит вас об этом. Если вы согласитесь, то он сделает автоматически написанное ниже.
## Вручную
Сделать `opkg update`, чтоб установились зависимости.
Скачать пакеты `podkop_*.ipk` и `luci-app-podkop_*.ipk` из релиза. `opkg install` сначала первый, потом второй.
При обновлении вручную нужно:
# Обновление
Та же самая команда, что для установки. Скрипт обнаружит уже установленный podkop и предложит обновиться.
0. Не ныть в issue и чатик.
1. Забэкапить старый конфиг:
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
mv /etc/config/podkop /etc/config/podkop-070
```
# Удаление
2. Стянуть новый дефолтный конфиг:
```
opkg remove luci-i18n-podkop-ru luci-app-podkop podkop
wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop
```
Если был установлен русский язык
```
opkg remove luci-i18n-podkop-ru
```
# Использование
Конфиг: /etc/config/podkop
Luci: Services/podkop
## Режимы
### Proxy
Для VLESS и Shadowsocks. Другие протоколы тоже будут, кидайте в чат примеры строк без чувствительных данных.
В этом режиме просто копируйте строку в **Proxy String** и из неё автоматически настроится sing-box.
### VPN
Здесь у вас должен быть уже настроен WG/OpenVPN/OpenConnect etc, зона Zone и Forwarding не обязательны.
Просто выбрать интерфейс из списка.
## Настройка доменов и подсетей
**Community Lists** - Включить списки комьюнити
**Subnets list enable** - Включить подсети из общего списка, выбрать из предложенных.
**Custom domains enable** - Добавить свои домены
**Custom subnets enable** - Добавить подсети или IP-адреса. Для подсетей задать маску.
# Известные баги
- [ ] Не отрабатывает service podkop stop, если podkop запущен и не может, к пример, зарезолвить домен с сломанным DNS
- [ ] Update list из remote url domain не удаляет старые домены. А добавляет новые. Для подсетей тоже самое скорее всего. Пересоздавать ruleset?
3. Настроить заново ваш Podkop через Luci или UCI.
# ToDo
Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме.
- [ ] Проверка, что версия в makefile совпадает с тегом
- [ ] Диагностика: Proxy check completed successfully предположительно не показывает IP, если вернулся это IPv6.
- [ ] Сделать галку запрещающую подкопу редачить dhcp. Допилить в исключение вместе с пустыми полями proxy и vpn
- [ ] Обработка ошибки `sing-box[9345]: FATAL[0000] start service: initialize DNS rule[2]: rule-set not found: main`. Когда не задана строка\интерфейс
> [!IMPORTANT]
> PR принимаются только по issues, у которых стоит label "enhancement". Либо по согласованию с авторами в ТГ-чате. Остальные PR на данный момент не рассматриваются.
Низкий приоритет
- [ ] Галочка, которая режет доступ к doh серверам
- [ ] Свой конфиг sing-box
- [ ] IPv6. Только после наполнения Wiki
## Будущее
- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров.
- [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне.
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусственно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111)
- [ ] Галочка, которая режет доступ к doh серверам.
- [ ] IPv6. Только после наполнения Wiki.
Рефактор
- [ ] Handle для sing-box
- [ ] Handle для dnsmasq
- [ ] Формирование json для sing-box на уровне jq, а не шаблонов
## Тесты
- [ ] Unit тесты (BATS)
- [ ] Интеграционые тесты бекенда (OpenWrt rootfs + BATS)
- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS)
# Разработка
Есть два варианта:
- Просто поставить пакет на роутер или виртуалку и прям редактировать через SFTP (opkg install openssh-sftp-server)
- SDK, чтоб собирать пакеты
Для сборки пакетов нужен SDK, один из вариантов скачать прям файл и разархивировать
https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/
Нужен файл с SDK в имени
```
wget https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz
tar xf openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz
mv openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64 SDK
```
Последнее для удобства.
Создаём директорию для пакета
```
mkdir package/utilites
```
Симлинк из репозитория
```
ln -s ~/podkop/podkop package/utilites/podkop
ln -s ~/podkop/luci-app-podkop package/luci-app-podkop
```
В первый раз для сборки luci-app необходимо обновить пакеты
```
./scripts/feeds update -a
```
Для make можно добавить флаг -j N, где N - количество ядер для сборки. Первый раз пройдёт быстрее.
При первом make выводится менюшка, можно просто save, exit и всё. Первый раз долго грузит зависимости.
Сборка пакета. Сами пакеты собираются быстро.
```
make package/podkop/{clean,compile} V=s
```
Также для luci
```
make package/luci-app-podkop/{clean,compile} V=s
```
.ipk лежат в `bin/packages/x86_64/base/`
## Примеры строкs
https://github.com/itdoginfo/podkop/blob/main/String-example.md
## Ошибки
```
Makefile:17: /SDK/feeds/luci/luci.mk: No such file or directory
make[2]: *** No rule to make target '/SDK/feeds/luci/luci.mk'. Stop.
time: package/luci/luci-app-podkop/clean#0.00#0.00#0.00
ERROR: package/luci/luci-app-podkop failed to build.
make[1]: *** [package/Makefile:129: package/luci/luci-app-podkop/clean] Error 1
make[1]: Leaving directory '/SDK'
make: *** [/SDK/include/toplevel.mk:226: package/luci-app-podkop/clean] Error 2
```
Не загружены пакеты для luci
## make зависимости
https://openwrt.org/docs/guide-developer/toolchain/install-buildsystem
Ubuntu
```
sudo apt update
sudo apt install build-essential clang flex bison g++ gawk \
gcc-multilib g++-multilib gettext git libncurses-dev libssl-dev \
python3-distutils rsync unzip zlib1g-dev file wget
```
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop)

View File

@@ -1,63 +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://eb445f4b-ddb4-4c79-86d5-0833fc674379@example.com:443?type=tcp&security=reality&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&fp=chrome&sni=yahoo.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://UUID@IP:2082?security=reality&sni=dash.cloudflare.com&alpn=h2,http/1.1&allowInsecure=1&fp=chrome&pbk=pukkey&sid=id&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://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443?security=tls&sni=SITE&fp=chrome&type=tcp&flow=xtls-rprx-vision&encryption=none#vless-tls-withot-alpn
```
3.
```
vless://8b60389a-7a01-4365-9244-c87f12bb98cf@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://[someid]@[someserver]?security=tls&sni=[somesni]&type=ws&path=/?ed%3D2560&host=[somesni]&encryption=none#vless-tls-ws-2
```
# With SNI
hy2://password@example.com:443/?sni=example.com#hysteria2-password-sni
5.
```
vless://uuid@server:443?security=tls&sni=server&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://33333@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=example.com&fp=chrome#vless-tls-ws-4
```
## No security
```
vless://8b60389a-7a01-4365-9244-c87f12bb98cf@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
```

View 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/

View File

@@ -0,0 +1,8 @@
{
"printWidth": 80,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true
}

View 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);
});

View 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,
];

View 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}`);

View 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);
});

View 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);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,774 @@
# RU translations for PODKOP package.
# Copyright (C) 2025 THE PODKOP'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PODKOP package.
# divocat, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: PODKOP\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-01 16:30+0200\n"
"PO-Revision-Date: 2025-12-01 16:30+0200\n"
"Last-Translator: divocat\n"
"Language-Team: none\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "✔ Enabled"
msgstr "✔ Включено"
msgid "✔ Running"
msgstr "✔ Работает"
msgid "✘ Disabled"
msgstr "✘ Отключено"
msgid "✘ Stopped"
msgstr "✘ Остановлен"
msgid "Active Connections"
msgstr "Активные соединения"
msgid "Additional marking rules found"
msgstr "Найдены дополнительные правила маркировки"
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
msgid "Applicable for SOCKS and Shadowsocks proxy"
msgstr "Применимо для SOCKS и Shadowsocks прокси"
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы."
msgid "Available actions"
msgstr "Доступные действия"
msgid "Bootsrap DNS"
msgstr "Bootstrap DNS"
msgid "Bootstrap DNS server"
msgstr "Bootstrap DNS-сервер"
msgid "Browser is not using FakeIP"
msgstr "Браузер не использует FakeIP"
msgid "Browser is using FakeIP correctly"
msgstr "Браузер использует FakeIP"
msgid "Cache File Path"
msgstr "Путь к файлу кэша"
msgid "Cache file path cannot be empty"
msgstr "Путь к файлу кэша не может быть пустым"
msgid "Cannot receive checks result"
msgstr "Не удалось получить результаты проверки"
msgid "Checking, please wait"
msgstr "Проверяем, пожалуйста подождите"
msgid "checks"
msgstr "проверки"
msgid "Checks failed"
msgstr "Проверки не выполнены"
msgid "Checks passed"
msgstr "Проверки пройдены"
msgid "CIDR must be between 0 and 32"
msgstr "CIDR должен быть между 0 и 32"
msgid "Close"
msgstr "Закрыть"
msgid "Community Lists"
msgstr "Списки сообщества"
msgid "Config File Path"
msgstr "Путь к файлу конфигурации"
msgid "Configuration for Podkop service"
msgstr "Настройки сервиса Podkop"
msgid "Configuration Type"
msgstr "Тип конфигурации"
msgid "Connection Type"
msgstr "Тип подключения"
msgid "Connection URL"
msgstr "URL подключения"
msgid "Copy"
msgstr "Копировать"
msgid "Currently unavailable"
msgstr "Временно недоступно"
msgid "Dashboard"
msgstr "Дашборд"
msgid "Dashboard currently unavailable"
msgstr "Дашборд сейчас недоступен"
msgid "Delay in milliseconds before reloading podkop after interface UP"
msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса"
msgid "Delay value cannot be empty"
msgstr "Значение задержки не может быть пустым"
msgid "DHCP has DNS server"
msgstr "DHCP содержит DNS сервер"
msgid "Diagnostics"
msgstr "Диагностика"
msgid "Disable autostart"
msgstr "Отключить автостарт"
msgid "Disable QUIC"
msgstr "Отключить QUIC"
msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming"
msgstr "Отключить QUIC протокол для улучшения совместимости или исправления видео стриминга"
msgid "Disabled"
msgstr "Отключено"
msgid "DNS on router"
msgstr "DNS на роутере"
msgid "DNS over HTTPS (DoH)"
msgstr "DNS через HTTPS (DoH)"
msgid "DNS over TLS (DoT)"
msgstr "DNS через TLS (DoT)"
msgid "DNS Protocol Type"
msgstr "Тип протокола DNS"
msgid "DNS Rewrite TTL"
msgstr "Перезапись TTL для DNS"
msgid "DNS Server"
msgstr "DNS-сервер"
msgid "DNS server address cannot be empty"
msgstr "Адрес DNS-сервера не может быть пустым"
msgid "Do not panic, everything can be fixed, just..."
msgstr "Не паникуйте, всё можно исправить, просто..."
msgid "Domain Resolver"
msgstr "Резолвер доменов"
msgid "Dont Touch My DHCP!"
msgstr "Dont Touch My DHCP!"
msgid "Downlink"
msgstr "Входящий"
msgid "Download"
msgstr "Скачать"
msgid "Download Lists via Proxy/VPN"
msgstr "Скачивать списки через Proxy/VPN"
msgid "Download Lists via specific proxy section"
msgstr "Скачивать списки через выбранную секцию"
msgid "Downloading all lists via specific Proxy/VPN"
msgstr "Загрузка всех списков через указанный прокси/VPN"
msgid "Dynamic List"
msgstr "Динамический список"
msgid "Enable autostart"
msgstr "Включить автостарт"
msgid "Enable built-in DNS resolver for domains handled by this section"
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе"
msgid "Enable Mixed Proxy"
msgstr "Включить смешанный прокси"
msgid "Enable Output Network Interface"
msgstr "Включить выходной сетевой интерфейс"
msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies"
msgstr "Включить смешанный прокси-сервер, разрешив этому разделу маршрутизировать трафик как через HTTP, так и через SOCKS-прокси."
msgid "Enable YACD"
msgstr "Включить YACD"
msgid "Enable YACD WAN Access"
msgstr "Включить доступ YACD WAN"
msgid "Enter complete outbound configuration in JSON format"
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //"
msgstr "Введите доменные имена, разделяя их запятыми, пробелами или переносами строк. Вы можете добавлять комментарии, используя //"
msgid "Enter domain names without protocols, e.g. example.com or sub.example.com"
msgstr "Введите доменные имена без протоколов, например example.com или sub.example.com"
msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses"
msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса"
msgid "Every 1 minute"
msgstr "Каждую минуту"
msgid "Every 3 minutes"
msgstr "Каждые 3 минуты"
msgid "Every 30 seconds"
msgstr "Каждые 30 секунд"
msgid "Every 5 minutes"
msgstr "Каждые 5 минут"
msgid "Exclude NTP"
msgstr "Исключить NTP"
msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN"
msgstr "Исключите трафик протокола NTP из туннеля, чтобы предотвратить его маршрутизацию через прокси-сервер или VPN."
msgid "Failed to copy!"
msgstr "Не удалось скопировать!"
msgid "Failed to execute!"
msgstr "Не удалось выполнить!"
msgid "Fastest"
msgstr "Самый быстрый"
msgid "Fully Routed IPs"
msgstr "Полностью маршрутизированные IP-адреса"
msgid "Get global check"
msgstr "Получить глобальную проверку"
msgid "Global check"
msgstr "Глобальная проверка"
msgid "HTTP error"
msgstr "Ошибка HTTP"
msgid "Interface Monitoring"
msgstr "Мониторинг интерфейса"
msgid "Interface Monitoring Delay"
msgstr "Задержка при мониторинге интерфейсов"
msgid "Interface monitoring for Bad WAN"
msgstr "Мониторинг интерфейса для Bad WAN"
msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH"
msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH"
msgid "Invalid domain address"
msgstr "Неверный домен"
msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y"
msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y"
msgid "Invalid 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 "Main DNS"
msgstr "Основной DNS"
msgid "Memory Usage"
msgstr "Использование памяти"
msgid "Mixed Proxy Port"
msgstr "Порт смешанного прокси"
msgid "Monitored Interfaces"
msgstr "Наблюдаемые интерфейсы"
msgid "Must be a number in the range of 50 - 1000"
msgstr "Должно быть числом от 50 до 1000"
msgid "Network Interface"
msgstr "Сетевой интерфейс"
msgid "No other marking rules found"
msgstr "Другие правила маркировки не найдены"
msgid "Not implement yet"
msgstr "Ещё не реализовано"
msgid "Not responding"
msgstr "Не отвечает"
msgid "Not running"
msgstr "Не запущено"
msgid "Operation timed out"
msgstr "Время ожидания истекло"
msgid "Outbound Config"
msgstr "Конфигурация Outbound"
msgid "Outbound Configuration"
msgstr "Конфигурация исходящего соединения"
msgid "Outdated"
msgstr "Устаревшая"
msgid "Output Network Interface"
msgstr "Выходной сетевой интерфейс"
msgid "Path cannot be empty"
msgstr "Путь не может быть пустым"
msgid "Path must be absolute (start with /)"
msgstr "Путь должен быть абсолютным (начинаться с /)"
msgid "Path must contain at least one directory (like /tmp/cache.db)"
msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)"
msgid "Path must end with cache.db"
msgstr "Путь должен заканчиваться на cache.db"
msgid "Pending"
msgstr "Ожидает запуска"
msgid "Podkop"
msgstr "Podkop"
msgid "Podkop Settings"
msgstr "Настройки podkop"
msgid "Podkop will not modify your DHCP configuration"
msgstr "Podkop не будет изменять вашу конфигурацию DHCP."
msgid "Proxy Configuration URL"
msgstr "URL конфигурации прокси"
msgid "Proxy traffic is not routed via FakeIP"
msgstr "Прокси-трафик не маршрутизируется через FakeIP"
msgid "Proxy traffic is routed via FakeIP"
msgstr "Прокси-трафик направляется через FakeIP"
msgid "Regional options cannot be used together"
msgstr "Нельзя использовать несколько региональных опций одновременно"
msgid "Remote Domain Lists"
msgstr "Внешние списки доменов"
msgid "Remote Subnet Lists"
msgstr "Внешние списки подсетей"
msgid "Restart podkop"
msgstr "Перезапустить Podkop"
msgid "Router DNS is not routed through sing-box"
msgstr "DNS роутера не проходит через sing-box"
msgid "Router DNS is routed through sing-box"
msgstr "DNS роутера проходит через sing-box"
msgid "Routing Excluded IPs"
msgstr "Исключённые из маршрутизации IP-адреса"
msgid "Rules mangle counters"
msgstr "Счётчики правил mangle"
msgid "Rules mangle exist"
msgstr "Правила mangle существуют"
msgid "Rules mangle output counters"
msgstr "Счётчики правил mangle output"
msgid "Rules mangle output exist"
msgstr "Правила mangle output существуют"
msgid "Rules proxy counters"
msgstr "Счётчики правил proxy"
msgid "Rules proxy exist"
msgstr "Правила прокси существуют"
msgid "Run Diagnostic"
msgstr "Запустить диагностику"
msgid "Russia inside restrictions"
msgstr "Ограничения Russia inside"
msgid "Secret key for authenticating remote access to YACD when WAN access is enabled."
msgstr "Секретный ключ для аутентификации удаленного доступа к YACD при включенном доступе через WAN."
msgid "Sections"
msgstr "Секции"
msgid "Select a predefined list for routing"
msgstr "Выберите предопределенный список для маршрутизации"
msgid "Select between VPN and Proxy connection methods for traffic routing"
msgstr "Выберите между VPN и Proxy методами для маршрутизации трафика"
msgid "Select DNS protocol to use"
msgstr "Выберите протокол DNS"
msgid "Select how often the domain or subnet lists are updated automatically"
msgstr "Выберите частоту автоматического обновления списков доменов или подсетей."
msgid "Select how to configure the proxy"
msgstr "Выберите способ настройки прокси"
msgid "Select network interface for VPN connection"
msgstr "Выберите сетевой интерфейс для VPN подключения"
msgid "Select or enter DNS server address"
msgstr "Выберите или введите адрес DNS-сервера"
msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing"
msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing"
msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
msgid "Select the DNS protocol type for the domain resolver"
msgstr "Выберите тип протокола DNS для резолвера доменов"
msgid "Select the list type for adding custom domains"
msgstr "Выберите тип списка для добавления пользовательских доменов"
msgid "Select the list type for adding custom subnets"
msgstr "Выберите тип списка для добавления пользовательских подсетей"
msgid "Select the network interface from which the traffic will originate"
msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик"
msgid "Select the network interface to which the traffic will originate"
msgstr "Выберите сетевой интерфейс, на который будет поступать трафик."
msgid "Select the WAN interfaces to be monitored"
msgstr "Выберите WAN интерфейсы для мониторинга"
msgid "Services info"
msgstr "Информация о сервисах"
msgid "Settings"
msgstr "Настройки"
msgid "Show sing-box config"
msgstr "Показать sing-box конфигурацию"
msgid "Sing-box"
msgstr "Sing-box"
msgid "Sing-box autostart disabled"
msgstr "Автостарт sing-box отключен"
msgid "Sing-box installed"
msgstr "Sing-box установлен"
msgid "Sing-box listening ports"
msgstr "Sing-box слушает порты"
msgid "Sing-box process running"
msgstr "Процесс sing-box запущен"
msgid "Sing-box service exist"
msgstr "Сервис sing-box существует"
msgid "Sing-box version is compatible (newer than 1.12.4)"
msgstr "Версия Sing-box совместима (новее 1.12.4)"
msgid "Source Network Interface"
msgstr "Сетевой интерфейс источника"
msgid "Specify a local IP address to be excluded from routing"
msgstr "Укажите локальный IP-адрес, который следует исключить из маршрутизации."
msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route"
msgstr "Укажите локальные IP-адреса или подсети, трафик которых всегда будет направляться через настроенный маршрут."
msgid "Specify remote URLs to download and use domain lists"
msgstr "Укажите URL-адреса для загрузки и использования списков доменов."
msgid "Specify remote URLs to download and use subnet lists"
msgstr "Укажите URL-адреса для загрузки и использования списков подсетей."
msgid "Specify the path to the list file located on the router filesystem"
msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора."
msgid "Start podkop"
msgstr "Запустить podkop"
msgid "Stop podkop"
msgstr "Остановить podkop"
msgid "Successfully copied!"
msgstr "Успешно скопировано!"
msgid "System info"
msgstr "Системная информация"
msgid "System information"
msgstr "Системная информация"
msgid "Table exist"
msgstr "Таблица существует"
msgid "Test latency"
msgstr "Тестирование задержки"
msgid "Text List"
msgstr "Текстовый список"
msgid "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 "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 "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически."

View 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:exctract-calls": "node extract-calls.js",
"locales:generate-pot": "node generate-pot.js",
"locales:generate-po:ru": "node generate-po.js ru",
"locales:distribute": "node distribute-locales.js",
"locales:actualize": "yarn locales:exctract-calls && yarn locales:generate-pot && yarn locales:generate-po:ru && yarn locales:distribute"
},
"devDependencies": {
"@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"
}
}

View File

@@ -0,0 +1,108 @@
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',
'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',
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;

View 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);
}

View 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);
}

View 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 };
}
}

View 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`;
}

View File

@@ -0,0 +1,13 @@
export function getProxyUrlName(url: string) {
try {
const [_link, hash] = url.split('#');
if (!hash) {
return '';
}
return decodeURIComponent(hash);
} catch {
return '';
}
}

View 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';

View File

@@ -0,0 +1,12 @@
import { GlobalStyles } from '../styles';
export function injectGlobalStyles() {
document.head.insertAdjacentHTML(
'beforeend',
`
<style>
${GlobalStyles}
</style>
`,
);
}

View 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);
}

View 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}`);
}

View File

@@ -0,0 +1,7 @@
export function normalizeCompiledVersion(version: string) {
if (version.includes('COMPILED')) {
return 'dev';
}
return version;
}

View 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,
});
});
}

View 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>,
);
}

View 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
}

View File

@@ -0,0 +1,9 @@
export function preserveScrollForPage(renderFn: () => void) {
const scrollY = window.scrollY;
renderFn();
requestAnimationFrame(() => {
window.scrollTo({ top: scrollY });
});
}

View 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;
}

View 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);
}

View File

@@ -0,0 +1,7 @@
export function splitProxyString(str: string) {
return str
.split('\n')
.map((line) => line.trim())
.filter((line) => !line.startsWith('//'))
.filter(Boolean);
}

View 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;
}

View 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('');
});
});

View 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`);
}
}

View 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';

View 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' }),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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' }),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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',
}),
],
);
}

View 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' }),
],
);
}

View 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' }),
],
);
}

View 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' }),
],
);
}

View 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
View 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
View 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';

View 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)],
);
}

View 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;
}
`;

View 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}
`;

View 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,
}),
]),
]),
);
}

View 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;
}
`;

View 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;
};

View 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 },
},
});
}
}

View File

@@ -0,0 +1 @@
export * from './fetchServicesInfo';

View File

@@ -0,0 +1,3 @@
export * from './methods';
export * from './services';
export * from './tabs';

View File

@@ -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 || '';
}

View File

@@ -0,0 +1,5 @@
import { Podkop } from '../../types';
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
return uci.load('podkop').then(() => uci.sections('podkop'));
}

View File

@@ -0,0 +1,161 @@
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['.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 === '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,
};
}

View File

@@ -0,0 +1,9 @@
import { getConfigSections } from './getConfigSections';
import { getDashboardSections } from './getDashboardSections';
import { getClashApiSecret } from './getClashApiSecret';
export const CustomPodkopMethods = {
getConfigSections,
getDashboardSections,
getClashApiSecret,
};

View 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,
},
);
}

View 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,
},
);
}

View File

@@ -0,0 +1,7 @@
import { getFakeIpCheck } from './getFakeIpCheck';
import { getIpCheck } from './getIpCheck';
export const RemoteFakeIPMethods = {
getFakeIpCheck,
getIpCheck,
};

View File

@@ -0,0 +1,3 @@
export * from './custom';
export * from './fakeip';
export * from './shell';

View 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: '',
};
}

View 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,
),
};

View 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();
}

View 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';

View 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();

View 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');
}
}

View 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();

View 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);

View 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();

View File

@@ -0,0 +1,9 @@
import { render } from './render';
import { initController } from './initController';
import { styles } from './styles';
export const DashboardTab = {
render,
initController,
styles,
};

View 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();
});
}

View File

@@ -0,0 +1,2 @@
export * from './renderSections';
export * from './renderWidget';

View File

@@ -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);
}

View File

@@ -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);
}

View 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,
}),
),
],
);
}

View File

@@ -0,0 +1,120 @@
// language=CSS
export const styles = `
#cbi-podkop-dashboard-_mount_node > div {
width: 100%;
}
#cbi-podkop-dashboard > h3 {
display: none;
}
.pdk_dashboard-page {
width: 100%;
--dashboard-grid-columns: 4;
}
@media (max-width: 900px) {
.pdk_dashboard-page {
--dashboard-grid-columns: 2;
}
}
.pdk_dashboard-page__widgets-section {
margin-top: 10px;
display: grid;
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
grid-gap: 10px;
}
.pdk_dashboard-page__widgets-section__item {
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
padding: 10px;
}
.pdk_dashboard-page__widgets-section__item__title {}
.pdk_dashboard-page__widgets-section__item__row {}
.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value {
color: var(--success-color-medium, green);
}
.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value {
color: var(--error-color-medium, red);
}
.pdk_dashboard-page__widgets-section__item__row__key {}
.pdk_dashboard-page__widgets-section__item__row__value {}
.pdk_dashboard-page__outbound-section {
margin-top: 10px;
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
padding: 10px;
}
.pdk_dashboard-page__outbound-section__title-section {
display: flex;
align-items: center;
justify-content: space-between;
}
.pdk_dashboard-page__outbound-section__title-section__title {
color: var(--text-color-high);
font-weight: 700;
}
.pdk_dashboard-page__outbound-grid {
margin-top: 5px;
display: grid;
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
grid-gap: 10px;
}
.pdk_dashboard-page__outbound-grid__item {
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
padding: 10px;
transition: border 0.2s ease;
}
.pdk_dashboard-page__outbound-grid__item--selectable {
cursor: pointer;
}
.pdk_dashboard-page__outbound-grid__item--selectable:hover {
border-color: var(--primary-color-high, dodgerblue);
}
.pdk_dashboard-page__outbound-grid__item--active {
border-color: var(--success-color-medium, green);
}
.pdk_dashboard-page__outbound-grid__item__footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.pdk_dashboard-page__outbound-grid__item__type {}
.pdk_dashboard-page__outbound-grid__item__latency--empty {
color: var(--primary-color-low, lightgray);
}
.pdk_dashboard-page__outbound-grid__item__latency--green {
color: var(--success-color-medium, green);
}
.pdk_dashboard-page__outbound-grid__item__latency--yellow {
color: var(--warn-color-medium, orange);
}
.pdk_dashboard-page__outbound-grid__item__latency--red {
color: var(--error-color-medium, red);
}
`;

Some files were not shown because too many files have changed in this diff Show More