Release 4.2.0+1: store-compliance формулировки и UX — убраны все цены/продажи из приложения (оффер «3 дня бесплатно» без ₽), «Активировать ключ» → «Добавить конфигурацию» (URL-ссылка, placeholder https://), блок «Ключ доступа» заменён статусом «Конфигурация добавлена/не добавлена» (URL не отображается), глобально Подписка→Конфигурация/Ваучер→URL-ссылка/Тариф→Профиль в UI и Go-строках; большая круглая кнопка ПОДКЛЮЧИТЬСЯ/ОТКЛЮЧИТЬСЯ со статусом, убран футер «Готово к подключению», протокол в списке — буквенный бейдж только при отличии от активного, трафик «ДОСТУПНО X ГБ» + «Активен до DD.MM.YYYY» + процент; пинг отдельного сервера кнопкой в строке, экран Статистика (за сегодня: трафик/время в сети/сессии + график 7 дней из localStorage), Kill Switch для VPN-режима (blackhole-маршруты сохраняются при обрыве туннеля, для прокси — системный прокси остаётся на мёртвый порт), тумблер «Авто-лучший сервер» в Настройках. Windows 4.2.0.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-08-01 22:34:43 +03:00
co-authored by Cursor
parent 74ffbd87d7
commit 2a57987aea
22 changed files with 670 additions and 127 deletions
+58 -3
View File
@@ -62,6 +62,10 @@ type UIState struct {
Mode string `json:"mode"`
VPNSupported bool `json:"vpn_mode_supported"`
VPNActive bool `json:"vpn_active"`
// KillSwitch is the persisted setting; KillSwitchActive means the tunnel
// died unexpectedly and traffic is blocked until disconnect/reconnect.
KillSwitch bool `json:"kill_switch"`
KillSwitchActive bool `json:"kill_switch_active,omitempty"`
}
type PingBestResult struct {
@@ -141,9 +145,11 @@ func (a *App) GetState() (UIState, error) {
Remnawave: a.Mgr.RemnawaveSettings(),
Hy2: a.Mgr.ActiveHy2Options(),
StorePackaged: update.IsStorePackaged(),
Mode: a.Mgr.Mode(),
VPNSupported: a.Mgr.VPNModeSupported(),
VPNActive: st.VPNActive,
Mode: a.Mgr.Mode(),
VPNSupported: a.Mgr.VPNModeSupported(),
VPNActive: st.VPNActive,
KillSwitch: cfg.KillSwitch,
KillSwitchActive: st.KillSwitchActive,
}
if out.Protocol == "" {
if p, err := cfg.ActiveProfile(); err == nil {
@@ -281,6 +287,51 @@ func (a *App) PingServers() ([]netcheck.Result, error) {
return a.runPings()
}
// PingServer measures latency of a single server (📊 button on a server row)
// and merges the result into the cached ping list.
func (a *App) PingServer(name string) (netcheck.Result, error) {
name = strings.TrimSpace(name)
a.mu.Lock()
list := a.Mgr.Profiles()
a.mu.Unlock()
var target *netcheck.Target
for _, p := range list {
if p.Name == name {
target = &netcheck.Target{Name: p.Name, Protocol: config.Protocol(p.Protocol), Proxy: p.Proxy}
break
}
}
if target == nil {
return netcheck.Result{}, fmt.Errorf("сервер %q не найден", name)
}
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()
out := netcheck.PingAll(ctx, []netcheck.Target{*target})
if len(out) == 0 {
return netcheck.Result{}, fmt.Errorf("не удалось измерить пинг")
}
res := out[0]
a.mu.Lock()
replaced := false
for i := range a.Pings {
if a.Pings[i].Name == res.Name {
a.Pings[i] = res
replaced = true
break
}
}
if !replaced {
a.Pings = append(a.Pings, res)
}
a.mu.Unlock()
return res, nil
}
// SetKillSwitch persists the leak-protection setting.
func (a *App) SetKillSwitch(enabled bool) error {
return a.Mgr.SetKillSwitch(enabled)
}
func (a *App) PingBest(autoConnect bool) (PingBestResult, error) {
pings, err := a.runPings()
out := PingBestResult{Pings: pings}
@@ -533,6 +584,10 @@ func (a *App) dispatch(name string, args []json.RawMessage) (any, error) {
return nil, a.OpenShopURL(arg(args, 0, ""))
case "pingServers":
return a.PingServers()
case "pingServer":
return a.PingServer(arg(args, 0, ""))
case "setKillSwitch":
return nil, a.SetKillSwitch(arg(args, 0, false))
case "pingBest":
return a.PingBest(arg(args, 0, false))
case "checkUpdate":