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
+55 -3
View File
@@ -64,6 +64,10 @@ type uiState struct {
Mode string `json:"mode"`
VPNSupported bool `json:"vpn_mode_supported"`
VPNActive bool `json:"vpn_active"`
// KillSwitch — persisted setting; KillSwitchActive — tunnel died and
// traffic is intentionally blocked until disconnect / reconnect.
KillSwitch bool `json:"kill_switch"`
KillSwitchActive bool `json:"kill_switch_active,omitempty"`
}
func main() {
@@ -146,7 +150,9 @@ func main() {
mustBind(w, "installCore", a.installCore)
mustBind(w, "openURL", openURL)
mustBind(w, "pingServers", a.pingServers)
mustBind(w, "pingServer", a.pingServer)
mustBind(w, "pingBest", a.pingBest)
mustBind(w, "setKillSwitch", a.setKillSwitch)
mustBind(w, "checkUpdate", a.checkUpdate)
mustBind(w, "applyUpdate", a.applyUpdate)
mustBind(w, "saveHy2", a.saveHy2)
@@ -225,9 +231,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 {
@@ -380,6 +388,50 @@ func (a *app) pingServers() ([]netcheck.Result, error) {
return res, err
}
// pingServer measures latency of one 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
}
func (a *app) setKillSwitch(enabled bool) error {
return a.mgr.SetKillSwitch(enabled)
}
// pingBest measures all nodes, activates the fastest OK one, and optionally connects.
func (a *app) pingBest(autoConnect bool) (pingBestResult, error) {
pings, err := a.runPings()