Release 3.8.2.11: idle rev-cache, port-safe Connect, single-instance tray UX.
ci / test (macos-latest) (push) Canceled after 0s
ci / test (ubuntu-latest) (push) Canceled after 0s
ci / test (windows-latest) (push) Canceled after 0s

Faster unchanged polls; orphan-core cleanup on listen ports; clearer busy-port errors; cores only from binDir; incremental server list; CSP without Google Fonts; single-instance activates existing window; close-to-tray without kill-others or balloon.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
M4
2026-08-01 17:27:47 +03:00
co-authored by Cursor
parent fed1ace33e
commit 57d719f02f
45 changed files with 876 additions and 285 deletions
+20 -4
View File
@@ -17,6 +17,7 @@ import (
"vpnclient/internal/core"
"vpnclient/internal/dockicon"
"vpnclient/internal/logbuf"
"vpnclient/internal/singleinstance"
"vpnclient/internal/trayhost"
"vpnclient/internal/update"
)
@@ -27,6 +28,16 @@ func main() {
}
update.CleanupStaleDownloads()
releaseInstance, err := singleinstance.Lock("Navis")
if err != nil {
if err == singleinstance.ErrAlreadyRunning {
// Existing window was activated when possible.
return
}
fatalf("Не удалось получить single-instance lock:\n%v", err)
}
defer releaseInstance()
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
@@ -66,14 +77,18 @@ func main() {
fatalf("listen: %v", err)
}
a.APIToken = token
srv := &http.Server{Handler: a.Handler()}
srv := &http.Server{
Handler: a.Handler(),
ReadHeaderTimeout: 5 * time.Second,
// No WriteTimeout: SSE /api/events is long-lived.
}
go func() {
_ = srv.Serve(ln)
}()
a.StartBackground()
dockicon.OnIconChange = trayhost.SetConnectedIcon
trayhost.Start("Navis", trayhost.Hooks{
trayOK := trayhost.Start("Navis", trayhost.Hooks{
Connect: func() { _ = a.Connect() },
Disconnect: func() { _ = a.Disconnect() },
Show: showMainWindow,
@@ -112,8 +127,9 @@ func main() {
w.SetTitle("Navis")
w.SetSize(500, 900, glaze.HintNone)
decorateWindow(w)
// Close→hide only when tray is alive; otherwise X would leave invisible zombies.
decorateWindow(w, trayOK)
w.Navigate(uiURL)
log.Printf("Navis UI: %s", uiURL)
log.Printf("Navis UI: %s (tray=%v hideOnClose=%v)", uiURL, trayOK, trayOK)
w.Run()
}
+3 -2
View File
@@ -27,9 +27,10 @@ func shutdownSignals() []os.Signal {
return []os.Signal{os.Interrupt, syscall.SIGTERM}
}
func decorateWindow(w glaze.WebView) {
// macOS Dock/app icon comes from the .app bundle; nothing to apply here.
func decorateWindow(w glaze.WebView, hideOnClose bool) {
// macOS Dock/app icon comes from the .app bundle; close-to-hide is Windows-only.
_ = w
_ = hideOnClose
}
func showMainWindow() {}
+4 -2
View File
@@ -64,14 +64,16 @@ func shutdownSignals() []os.Signal {
return []os.Signal{os.Interrupt}
}
func decorateWindow(w glaze.WebView) {
func decorateWindow(w glaze.WebView, hideOnClose bool) {
if w == nil {
return
}
hwnd := w.Window()
applyWindowIcon(hwnd)
dockicon.BindWindow(hwnd)
installCloseToHide(hwnd)
if hideOnClose {
installCloseToHide(hwnd)
}
}
func installCloseToHide(hwnd unsafe.Pointer) {