From 6e23ed4da75fd5eb50c938c581e66140029ac698 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 8 Jun 2026 17:10:15 +0200 Subject: [PATCH 1/4] [client] Add error event publishing for rejected session deadlines (#6358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [client] Surface session deadline rejections via SystemEvent and add timer arm debug logs When sessionwatch.Watcher.Update rejects a deadline (pre-epoch, too far in the future, or past the clock-skew tolerance) it silently zeroes the status recorder, leaving the UI with no "expires in" row and no indication of why. Publish a SystemEvent_ERROR on the AUTHENTICATION channel so the rejection appears in the UI event feed and the user knows re-login may be required. Also add Debugf log lines in armTimerLocked so that warning and final-warning timer fire-times are visible in logs without having to add instrumentation after the fact. https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u * [client] Remove verbose arm-timer debug logs from sessionwatch The per-arm Debugf lines added noise on every deadline update. Rejection logging already happens at the call site in engine_authsession.go; the watcher itself needs no extra instrumentation. https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u * [client] Leave userMessage empty on deadline-rejected event; use metadata key Daemon-layer PublishEvent userMessage strings are not localized — the UI reads metadata keys and builds its own locale-aware copy (same pattern as the session-warning events in event.go). Drop the hardcoded English sentence from the deadline-rejected event and instead surface the rejection reason via a new MetaSessionDeadlineRejected metadata key so the UI can detect and localize it. https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u * [client] Revert silent deadline-rejected event; restore userMessage MetaSessionDeadlineRejected had no UI consumer: the tray only does metadata-driven localisation for MetaSessionWarning events; all other SystemEvents display userMessage directly (tray_events.go). Leaving userMessage empty made the rejection invisible to the user. Restore the English userMessage so the generic event path shows something, and remove the unused MetaSessionDeadlineRejected constant. https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u * [client] Localize session deadline rejected notification via metadata key Follow the same pattern as session-warning events: the daemon emits an empty userMessage and puts the signal in a typed metadata key (MetaSessionDeadlineRejected); the UI tray detects the key and builds a locale-aware OS notification from i18n strings. Changes: - sessionwatch/event.go: add MetaSessionDeadlineRejected constant - engine_authsession.go: empty userMessage, use the new metadata key - ui/authsession/warning.go: re-export MetaDeadlineRejected for UI consumers - ui/tray_events.go: gate on isDeadlineRejected alongside isSessionWarning; new branch calls t.notify with localized title/body - i18n locales (en/de/hu): add notify.sessionDeadlineRejected.{title,body} https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u --------- Co-authored-by: Claude --- client/internal/auth/sessionwatch/event.go | 8 ++++++++ client/internal/engine_authsession.go | 9 +++++++++ client/ui/authsession/warning.go | 9 +++++---- client/ui/i18n/locales/de/common.json | 2 ++ client/ui/i18n/locales/en/common.json | 2 ++ client/ui/i18n/locales/hu/common.json | 2 ++ client/ui/tray_events.go | 18 ++++++++++++++---- 7 files changed, 42 insertions(+), 8 deletions(-) diff --git a/client/internal/auth/sessionwatch/event.go b/client/internal/auth/sessionwatch/event.go index 7a0ec8ce7..3e55b26dd 100644 --- a/client/internal/auth/sessionwatch/event.go +++ b/client/internal/auth/sessionwatch/event.go @@ -33,6 +33,14 @@ const ( // for the T-10 event, FinalWarningLead for the T-2 event) so the UI // can show "expires in ~N minutes" without hardcoding either constant. MetaSessionLeadMinutes = "lead_minutes" + // MetaSessionDeadlineRejected is attached to the ERROR/AUTHENTICATION + // SystemEvent the daemon emits when it discards a deadline from the + // management server (pre-epoch, too far in the future, or past the + // clock-skew tolerance). The value is the rejection reason string. + // userMessage is left empty; the UI detects the event via this key + // and builds a localized notification — same pattern as the session + // warnings above. + MetaSessionDeadlineRejected = "session_deadline_rejected" ) // expiresAtLayout is the wire format used for MetaSessionExpiresAt. diff --git a/client/internal/engine_authsession.go b/client/internal/engine_authsession.go index 49e276b77..8335c2cb5 100644 --- a/client/internal/engine_authsession.go +++ b/client/internal/engine_authsession.go @@ -9,6 +9,8 @@ import ( log "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/timestamppb" + cProto "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/client/internal/auth/sessionwatch" "github.com/netbirdio/netbird/client/system" ) @@ -51,6 +53,13 @@ func (e *Engine) ApplySessionDeadline(ts *timestamppb.Timestamp) { // of sync with the warning timers. if err := e.sessionWatcher.Update(deadline); err != nil { log.Errorf("auth session deadline rejected: %v, clearing", err) + e.statusRecorder.PublishEvent( + cProto.SystemEvent_ERROR, + cProto.SystemEvent_AUTHENTICATION, + "session deadline rejected", + "", + map[string]string{sessionwatch.MetaSessionDeadlineRejected: err.Error()}, + ) } } diff --git a/client/ui/authsession/warning.go b/client/ui/authsession/warning.go index a9e8fa65f..304b903d2 100644 --- a/client/ui/authsession/warning.go +++ b/client/ui/authsession/warning.go @@ -19,10 +19,11 @@ import ( // side) so UI-side consumers don't have to import the daemon-internal // package directly. const ( - MetaWarning = sessionwatch.MetaSessionWarning - MetaFinal = sessionwatch.MetaSessionFinal - MetaExpiresAt = sessionwatch.MetaSessionExpiresAt - MetaLeadMinutes = sessionwatch.MetaSessionLeadMinutes + MetaWarning = sessionwatch.MetaSessionWarning + MetaFinal = sessionwatch.MetaSessionFinal + MetaExpiresAt = sessionwatch.MetaSessionExpiresAt + MetaLeadMinutes = sessionwatch.MetaSessionLeadMinutes + MetaDeadlineRejected = sessionwatch.MetaSessionDeadlineRejected ) // Warning is the typed payload emitted on the session-warning Wails diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json index 2db418475..841dafeeb 100644 --- a/client/ui/i18n/locales/de/common.json +++ b/client/ui/i18n/locales/de/common.json @@ -55,6 +55,8 @@ "notify.sessionWarning.failed": "NetBird-Sitzung konnte nicht verlängert werden", "notify.sessionWarning.successTitle": "NetBird-Sitzung verlängert", "notify.sessionWarning.successBody": "Ihre Sitzung wurde erneuert.", + "notify.sessionDeadlineRejected.title": "Sitzungsfrist abgelehnt", + "notify.sessionDeadlineRejected.body": "Der Server hat eine ungültige Sitzungsfrist übermittelt. Bitte melden Sie sich erneut an.", "common.cancel": "Abbrechen", "common.save": "Speichern", diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json index 2698e2b12..f88cf3555 100644 --- a/client/ui/i18n/locales/en/common.json +++ b/client/ui/i18n/locales/en/common.json @@ -55,6 +55,8 @@ "notify.sessionWarning.failed": "Failed to extend NetBird session", "notify.sessionWarning.successTitle": "NetBird session extended", "notify.sessionWarning.successBody": "Your session has been refreshed.", + "notify.sessionDeadlineRejected.title": "Session deadline rejected", + "notify.sessionDeadlineRejected.body": "The server sent an invalid session deadline. Please sign in again.", "common.cancel": "Cancel", "common.save": "Save", diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json index af6cfdea6..127dc5da9 100644 --- a/client/ui/i18n/locales/hu/common.json +++ b/client/ui/i18n/locales/hu/common.json @@ -55,6 +55,8 @@ "notify.sessionWarning.failed": "A NetBird munkamenet meghosszabbítása sikertelen", "notify.sessionWarning.successTitle": "NetBird munkamenet meghosszabbítva", "notify.sessionWarning.successBody": "A munkamenet frissítve.", + "notify.sessionDeadlineRejected.title": "Munkamenet-határidő elutasítva", + "notify.sessionDeadlineRejected.body": "A szerver érvénytelen munkamenet-határidőt küldött. Kérjük, jelentkezzen be újra.", "common.cancel": "Mégse", "common.save": "Mentés", diff --git a/client/ui/tray_events.go b/client/ui/tray_events.go index 1a628f0bc..4668d555d 100644 --- a/client/ui/tray_events.go +++ b/client/ui/tray_events.go @@ -22,11 +22,12 @@ func (t *Tray) onSystemEvent(ev *application.CustomEvent) { if !ok { return } - // Session-warning events carry no UserMessage — the tray builds the - // localised notification body locally from metadata. Every other - // event needs a non-empty UserMessage to show anything meaningful. + // Session-warning and deadline-rejected events carry no UserMessage — + // the tray builds the localised notification body locally from metadata. + // Every other event needs a non-empty UserMessage to show anything meaningful. isSessionWarning := se.Metadata[authsession.MetaWarning] == "true" - if !isSessionWarning && se.UserMessage == "" { + isDeadlineRejected := se.Metadata[authsession.MetaDeadlineRejected] != "" + if !isSessionWarning && !isDeadlineRejected && se.UserMessage == "" { return } if shouldSkipSystemEvent(se) { @@ -52,6 +53,15 @@ func (t *Tray) onSystemEvent(ev *application.CustomEvent) { // - T-FinalWarningLead (MetaSessionFinal=true) → auto-open the // SessionAboutToExpire dialog. No OS notification here; the // dialog is the last-chance reminder, doubling up would be noise. + if isDeadlineRejected { + t.notify( + t.loc.T("notify.sessionDeadlineRejected.title"), + t.loc.T("notify.sessionDeadlineRejected.body"), + notifyIDSessionExpired, + ) + return + } + if se.Metadata != nil && se.Metadata[authsession.MetaWarning] == "true" { if se.Metadata[authsession.MetaFinal] == "true" { t.openSessionAboutToExpire() From ee912e176a2133fb74ce477a43a2650dee3e6354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Mon, 8 Jun 2026 17:14:38 +0200 Subject: [PATCH 2/4] ci: skip translated locales in codespell with version-stable globs The recursive ** skip pattern did not take effect with the codespell shipped by the action, so de/common.json still tripped on real German words. Use single-star globs (matched per path segment across versions) and skip every translated locale, keeping only en as source of truth. --- .github/workflows/golangci-lint.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e13bcf3a2..9c299dfc6 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -24,9 +24,13 @@ jobs: ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA,ede,additionals # Non-English UI translations trip codespell on real foreign words # (de: "Sie", "oder", "ist"). Only en/common.json is the source of - # truth that should be spell-checked. Add each new locale dir here - # when a language is added under client/ui/i18n/locales/. - skip: go.mod,go.sum,**/proxy/web/**,**/pnpm-lock.yaml,**/package-lock.json,client/ui/i18n/locales/de/**,client/ui/i18n/locales/hu/** + # truth that should be spell-checked. List each translated locale + # dir below and add new ones as languages are added under + # client/ui/i18n/locales/. Single-star globs are matched per path + # segment by codespell and behave the same across versions; the + # recursive "**" form did not take effect with the codespell shipped + # by this action. + skip: go.mod,go.sum,*/proxy/web/*,*pnpm-lock.yaml,*package-lock.json,*/locales/de/*,*/locales/hu/* golangci: strategy: fail-fast: false From b598274424c5f035dedcd001cf1b61497223149d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Mon, 8 Jun 2026 17:26:54 +0200 Subject: [PATCH 3/4] [client/ui] Refresh profile list on CLI profile select SwitchProfile now publishes the same profile-list-changed event that AddProfile/RemoveProfile already emit. The daemon emits no dedicated profile RPC event, and the React ProfileContext only refreshes on EventProfileChanged (unlike the tray, which also re-fetches on every status-string transition via loadProfiles). So a CLI-driven "netbird down; profile select X; netbird up" refreshed the tray (the down/up status flips trigger loadProfiles) but left the React profile dropdown stale, since the select path never surfaced an event. Publishing the marked INFO/SYSTEM event from SwitchProfile closes that gap: dispatchSystemEvent re-emits EventProfileChanged, which ProfileContext.refresh already subscribes to. No proto change. --- client/server/server.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/server/server.go b/client/server/server.go index 685314819..20f80743e 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -959,6 +959,10 @@ func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfi s.config = config + if msg != nil && msg.ProfileName != nil { + s.publishProfileListChanged(*msg.ProfileName) + } + return &proto.SwitchProfileResponse{}, nil } From 43175e07309f1e97a6f86f37582e8de1d90f2602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Mon, 8 Jun 2026 17:34:18 +0200 Subject: [PATCH 4/4] Move no-op rationale into empty function bodies Replace the doc comments above the empty no-op stubs (bindTrayClick, startTrayTheme, noopSessionWatcher.Dismiss/Close) with short in-body comments explaining why each is empty. --- client/internal/engine_sessionwatch_js.go | 9 +++++++-- client/ui/tray_click_other.go | 13 ++++--------- client/ui/tray_theme_other.go | 11 ++++------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/client/internal/engine_sessionwatch_js.go b/client/internal/engine_sessionwatch_js.go index 98a0c1da7..50e148ab9 100644 --- a/client/internal/engine_sessionwatch_js.go +++ b/client/internal/engine_sessionwatch_js.go @@ -35,5 +35,10 @@ func (w noopSessionWatcher) Update(deadline time.Time) error { return nil } -func (noopSessionWatcher) Dismiss() {} -func (noopSessionWatcher) Close() {} +func (noopSessionWatcher) Dismiss() { + // No-op: only suppresses the timer-driven final-warning, which this stub never arms. +} + +func (noopSessionWatcher) Close() { + // No-op: no timers to stop and no state to unwind; the recorder is cleared via Update(zero). +} diff --git a/client/ui/tray_click_other.go b/client/ui/tray_click_other.go index 231986a47..df2dc5afe 100644 --- a/client/ui/tray_click_other.go +++ b/client/ui/tray_click_other.go @@ -2,12 +2,7 @@ package main -// bindTrayClick is a no-op on macOS and Linux. On macOS the native -// NSStatusItem auto-shows the menu on left-click; on Linux the -// StatusNotifierItem host paints the menu independently. Binding an -// OnClick→OpenMenu handler is both unnecessary there and actively harmful on -// macOS, where OpenMenu routes through NSStatusItem's blocking [button -// mouseDown:] on the serial main GCD queue and freezes the tray and webview -// until the menu closes (commit c77e5cef8). Windows opts in via the sibling -// tray_click_windows.go file. -func bindTrayClick(*Tray) {} +func bindTrayClick(*Tray) { + // No-op: macOS/Linux native trays open the menu on click themselves. + // Only Windows needs an explicit handler (tray_click_windows.go). +} diff --git a/client/ui/tray_theme_other.go b/client/ui/tray_theme_other.go index a0d20f7ab..86a8eae73 100644 --- a/client/ui/tray_theme_other.go +++ b/client/ui/tray_theme_other.go @@ -2,10 +2,7 @@ package main -// startTrayTheme is a no-op off Linux: macOS uses template icons (the OS -// recolours them per menubar appearance) and Windows ships colored PNGs, so -// neither needs the freedesktop colour-scheme probe that the Linux build -// uses to choose between the black and white monochrome silhouettes. Left -// callable so NewTray can invoke it unconditionally; panelDark stays nil and -// panelIsDark returns its default. -func (t *Tray) startTrayTheme() {} +func (t *Tray) startTrayTheme() { + // No-op off Linux: macOS template icons and Windows colored PNGs need no + // colour-scheme probe. panelDark stays nil; panelIsDark uses its default. +}