Merge branch 'main' into file-share

# Conflicts:
#	client/ios/NetBirdSDK/client.go
This commit is contained in:
Zoltán Papp
2026-09-01 18:06:26 +02:00
485 changed files with 36221 additions and 5501 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
# docker run --rm -v $(pwd):/app wails-cross windows amd64
# docker run --rm -v $(pwd):/app wails-cross windows arm64
FROM golang:1.25-bookworm
FROM golang:1.26.7-bookworm
ARG TARGETARCH
+1 -1
View File
@@ -2,7 +2,7 @@
# Multi-stage build for minimal image size
# Build stage
FROM golang:alpine AS builder
FROM golang:1.26.7-alpine AS builder
WORKDIR /app
@@ -18,6 +18,11 @@ import { formatRemaining } from "@/lib/formatters";
const DEFAULT_SECONDS = 360;
const WINDOW_WIDTH = 360;
const SOON_THRESHOLD_SECONDS = 60 * 60;
const DEADLINE_TOLERANCE_MS = 5 * 1000;
// The final-warning deadline reaches the Go side as RFC3339 truncated to whole
// seconds, while the status snapshot carries millisecond precision, so an
// unchanged deadline can look up to 999 ms newer than the exact URL value.
const EXACT_DEADLINE_TOLERANCE_MS = 999;
export default function SessionExpirationDialog() {
const { t } = useTranslation();
@@ -29,11 +34,19 @@ export default function SessionExpirationDialog() {
const n = Number.parseInt(raw, 10);
return Number.isFinite(n) && n > 0 ? n : DEFAULT_SECONDS;
}, [params]);
const initialDeadline = useMemo(() => {
const raw = params.get("deadline");
if (!raw) return null;
const n = Number.parseInt(raw, 10);
return Number.isFinite(n) && n > 0 ? n : null;
}, [params]);
const [remaining, setRemaining] = useState(initialSeconds);
const [busy, setBusy] = useState(false);
const busyRef = useRef(busy);
busyRef.current = busy;
const openedDeadlineRef = useRef(initialDeadline ?? Date.now() + initialSeconds * 1000);
const exactDeadlineRef = useRef(initialDeadline !== null);
const expired = remaining <= 0;
const expiredRef = useRef(expired);
expiredRef.current = expired;
@@ -45,23 +58,45 @@ export default function SessionExpirationDialog() {
useEffect(() => {
setRemaining(initialSeconds);
}, [initialSeconds]);
openedDeadlineRef.current = initialDeadline ?? Date.now() + initialSeconds * 1000;
exactDeadlineRef.current = initialDeadline !== null;
}, [initialSeconds, initialDeadline]);
// Recompute from the absolute deadline instead of decrementing per tick: webview
// timers get suspended for tens of seconds (App Nap / hidden-window throttling),
// so a tick counter drifts behind the wall clock by the suspended time.
useEffect(() => {
const id = globalThis.setInterval(() => {
setRemaining((s) => (s <= 1 ? 0 : s - 1));
setRemaining(Math.max(0, Math.ceil((openedDeadlineRef.current - Date.now()) / 1000)));
}, 1000);
return () => globalThis.clearInterval(id);
}, [initialSeconds]);
// Auto-close only when the session was actually renewed elsewhere (tray action, CLI,
// main window): the daemon keeps emitting Connected snapshots regardless of session
// state, so the signal is the deadline jumping past the one this dialog was opened for.
// With the exact deadline from the URL any jump past its sub-second precision loss
// counts; the seconds-derived fallback needs a wider tolerance for the Go-side
// truncation and mount latency.
// Don't auto-close while busy (aborts our WaitExtend) or expired (hides the state).
useEffect(() => {
const off = Events.On("netbird:status", (ev: { data: { status?: string } }) => {
if (busyRef.current || expiredRef.current) return;
if (ev?.data?.status === "Connected") {
WindowManager.CloseSessionExpiration().catch(console.error);
}
});
const off = Events.On(
"netbird:status",
(ev: { data: { status?: string; sessionExpiresAt?: string | null } }) => {
if (busyRef.current || expiredRef.current) return;
if (ev?.data?.status !== "Connected") return;
const raw = ev?.data?.sessionExpiresAt;
if (!raw) return;
const renewed = Date.parse(raw);
if (!Number.isFinite(renewed)) return;
const tolerance = exactDeadlineRef.current
? EXACT_DEADLINE_TOLERANCE_MS
: DEADLINE_TOLERANCE_MS;
if (renewed - openedDeadlineRef.current > tolerance) {
WindowManager.CloseSessionExpiration().catch(console.error);
}
},
);
return () => {
off();
};
+1
View File
@@ -1,6 +1,7 @@
{
"languages": [
{"code": "en", "displayName": "English (US)", "englishName": "English (US)"},
{"code": "uk", "displayName": "Українська", "englishName": "Ukrainian"},
{"code": "de", "displayName": "Deutsch", "englishName": "German"},
{"code": "hu", "displayName": "Magyar", "englishName": "Hungarian"},
{"code": "ru", "displayName": "Русский", "englishName": "Russian"},
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -292,11 +292,15 @@ func (s *WindowManager) CloseBrowserLogin() {
}
// OpenSessionExpiration shows the countdown warning on the cursor's display; seconds seeds
// the countdown. Singleton, destroyed on close.
func (s *WindowManager) OpenSessionExpiration(seconds int) {
// the countdown and deadlineUnixMilli (0 when unknown) is the absolute deadline the dialog
// compares renewal snapshots against. Singleton, destroyed on close.
func (s *WindowManager) OpenSessionExpiration(seconds int, deadlineUnixMilli int64) {
s.mu.Lock()
defer s.mu.Unlock()
startURL := "/#/dialog/session-expiration?seconds=" + strconv.Itoa(seconds)
if deadlineUnixMilli > 0 {
startURL += "&deadline=" + strconv.FormatInt(deadlineUnixMilli, 10)
}
if s.sessionExpiration == nil {
opts := DialogWindowOptions("session-expiration", s.title("window.title.sessionExpiration"), startURL, s.linuxIcon)
opts.Screen = s.getScreenBasedOnCursorPosition()
+2 -1
View File
@@ -81,7 +81,8 @@ func (t *Tray) onSystemEvent(ev *application.CustomEvent) {
if se.Metadata != nil && se.Metadata[authsession.MetaWarning] == "true" {
if se.Metadata[authsession.MetaFinal] == "true" {
t.openSessionExpiration()
deadline, _ := authsession.ParseExpiresAt(se.Metadata[authsession.MetaExpiresAt])
t.openSessionExpiration(deadline)
return
}
t.notifySessionWarning(
+15 -4
View File
@@ -278,12 +278,23 @@ func (t *Tray) dismissSessionWarning() {
}
// openSessionExpiration fires the fallback dialog when the earlier warning notification wasn't dismissed.
// Idempotent on the WindowManager side.
func (t *Tray) openSessionExpiration() {
// deadline is the absolute expiry from the warning event's metadata; when zero (older daemon,
// malformed metadata) the cached status-snapshot deadline fills in. Idempotent on the
// WindowManager side.
func (t *Tray) openSessionExpiration(deadline time.Time) {
if t.svc.WindowManager == nil {
return
}
t.svc.WindowManager.OpenSessionExpiration(finalWarningCountdownSeconds)
if deadline.IsZero() {
t.sessionMu.Lock()
deadline = t.sessionExpiresAt
t.sessionMu.Unlock()
}
var deadlineMs int64
if !deadline.IsZero() {
deadlineMs = deadline.UnixMilli()
}
t.svc.WindowManager.OpenSessionExpiration(finalWarningCountdownSeconds, deadlineMs)
}
// openSessionExtendFlow opens the SessionExpiration window seeded with the cached deadline's remaining time,
@@ -304,5 +315,5 @@ func (t *Tray) openSessionExtendFlow() {
if t.svc.WindowManager == nil {
return
}
t.svc.WindowManager.OpenSessionExpiration(seconds)
t.svc.WindowManager.OpenSessionExpiration(seconds, deadline.UnixMilli())
}