From d3b660afbab077e7279f44782c02816462724c23 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 20 May 2026 19:13:13 +0200 Subject: [PATCH] classify daemon login errors and surface localised dialogs The daemon returns gRPC errors whose message is a wrapped mgm + JWT stack (e.g. "invalid jwt token, err: token could not be parsed: ..."). Showing that in a native dialog is unreadable. Connection now maps the substrings it recognises to a ClientError{code, short, long} so the UI can render a localised summary plus a Details: block carrying the raw daemon text. formatErrorMessage on the TS side reads the structured payload from Wails' Error.cause (or the JSON-stringified Error.message) and falls back to plain Error.message for callers not yet migrated. Also bumps Wails to v3.0.0-alpha.95. --- .../src/components/ProfileDropdown.tsx | 5 +- .../frontend/src/i18n/locales/de/common.json | 11 +- .../frontend/src/i18n/locales/en/common.json | 11 +- .../frontend/src/i18n/locales/hu/common.json | 11 +- .../src/layouts/ConnectionStatusSwitch.tsx | 4 +- client/ui/frontend/src/lib/errors.ts | 52 ++++++ .../SessionAboutToExpireDialog.tsx | 5 +- .../modules/debug-bundle/useDebugBundle.ts | 3 +- .../src/modules/settings/LanguagePicker.tsx | 3 +- .../src/modules/settings/SettingsContext.tsx | 4 +- .../src/modules/settings/SettingsProfiles.tsx | 5 +- client/ui/frontend/src/screens/Update.tsx | 3 +- client/ui/main.go | 7 +- client/ui/services/connection.go | 162 ++++++++++++++++-- go.mod | 4 +- go.sum | 8 +- 16 files changed, 262 insertions(+), 36 deletions(-) create mode 100644 client/ui/frontend/src/lib/errors.ts diff --git a/client/ui/frontend/src/components/ProfileDropdown.tsx b/client/ui/frontend/src/components/ProfileDropdown.tsx index 0b70c2d0b..dc1cafa95 100644 --- a/client/ui/frontend/src/components/ProfileDropdown.tsx +++ b/client/ui/frontend/src/components/ProfileDropdown.tsx @@ -11,6 +11,7 @@ import { NewProfileModal } from "@/components/NewProfileModal"; import { Tooltip } from "@/components/Tooltip"; import { useProfile } from "@/modules/profile/ProfileContext"; import { cn } from "@/lib/cn"; +import { formatErrorMessage } from "@/lib/errors"; type ProfileDropdownProps = { onManageProfiles?: () => void; @@ -40,7 +41,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { } catch (e) { await Dialogs.Error({ Title: title, - Message: e instanceof Error ? e.message : String(e), + Message: formatErrorMessage(e), }); } finally { setBusy(false); @@ -70,7 +71,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { } catch (e) { await Dialogs.Error({ Title: t("profile.error.createTitle"), - Message: e instanceof Error ? e.message : String(e), + Message: formatErrorMessage(e), }); } }; diff --git a/client/ui/frontend/src/i18n/locales/de/common.json b/client/ui/frontend/src/i18n/locales/de/common.json index 075c46b32..9ff26ea1b 100644 --- a/client/ui/frontend/src/i18n/locales/de/common.json +++ b/client/ui/frontend/src/i18n/locales/de/common.json @@ -293,5 +293,14 @@ "daemon.unavailable.title": "NetBird-Dienst läuft nicht", "daemon.unavailable.description": "Die App stellt automatisch die Verbindung wieder her, sobald der Dienst läuft.", - "daemon.unavailable.docsLink": "Dokumentation" + "daemon.unavailable.docsLink": "Dokumentation", + + "error.jwt_clock_skew": "Anmeldung fehlgeschlagen: Die Uhr dieses Geräts ist nicht mit dem Server synchron. Bitte synchronisieren Sie die Systemuhr und versuchen Sie es erneut.", + "error.jwt_expired": "Ihr Anmeldetoken ist abgelaufen. Bitte melden Sie sich erneut an.", + "error.jwt_signature_invalid": "Anmeldung fehlgeschlagen: Die Token-Signatur ist ungültig. Bitte wenden Sie sich an Ihren Administrator.", + "error.session_expired": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", + "error.invalid_setup_key": "Der Setup-Schlüssel fehlt oder ist ungültig.", + "error.permission_denied": "Die Anmeldung wurde vom Server abgelehnt.", + "error.daemon_unreachable": "Der NetBird-Dienst antwortet nicht. Bitte prüfen Sie, ob der Dienst läuft.", + "error.unknown": "Vorgang fehlgeschlagen. Technische Details siehe unten." } diff --git a/client/ui/frontend/src/i18n/locales/en/common.json b/client/ui/frontend/src/i18n/locales/en/common.json index 375755ef6..82ad6130a 100644 --- a/client/ui/frontend/src/i18n/locales/en/common.json +++ b/client/ui/frontend/src/i18n/locales/en/common.json @@ -314,5 +314,14 @@ "daemon.unavailable.title": "NetBird Service Is Not Running", "daemon.unavailable.description": "The app will reconnect automatically once the service is running.", - "daemon.unavailable.docsLink": "Documentation" + "daemon.unavailable.docsLink": "Documentation", + + "error.jwt_clock_skew": "Sign-in failed: this device's clock is out of sync with the server. Please sync your system clock and try again.", + "error.jwt_expired": "Your sign-in token has expired. Please sign in again.", + "error.jwt_signature_invalid": "Sign-in failed: the token signature is invalid. Please contact your administrator.", + "error.session_expired": "Your session has expired. Please sign in again.", + "error.invalid_setup_key": "The setup key is missing or invalid.", + "error.permission_denied": "Sign-in was rejected by the server.", + "error.daemon_unreachable": "The NetBird daemon is not responding. Please check that the service is running.", + "error.unknown": "Operation failed. See details for the technical message." } diff --git a/client/ui/frontend/src/i18n/locales/hu/common.json b/client/ui/frontend/src/i18n/locales/hu/common.json index 3f764e98d..e02a2ba18 100644 --- a/client/ui/frontend/src/i18n/locales/hu/common.json +++ b/client/ui/frontend/src/i18n/locales/hu/common.json @@ -293,5 +293,14 @@ "daemon.unavailable.title": "A NetBird szolgáltatás nem fut", "daemon.unavailable.description": "Az alkalmazás automatikusan újracsatlakozik, amint a szolgáltatás újra elérhető.", - "daemon.unavailable.docsLink": "Dokumentáció" + "daemon.unavailable.docsLink": "Dokumentáció", + + "error.jwt_clock_skew": "A bejelentkezés sikertelen: az eszköz órája eltér a szerverétől. Kérjük, szinkronizálja a rendszer óráját, majd próbálja újra.", + "error.jwt_expired": "A bejelentkezési token lejárt. Kérjük, jelentkezzen be újra.", + "error.jwt_signature_invalid": "A bejelentkezés sikertelen: a token aláírása érvénytelen. Kérjük, lépjen kapcsolatba a rendszergazdával.", + "error.session_expired": "A munkamenet lejárt. Kérjük, jelentkezzen be újra.", + "error.invalid_setup_key": "A telepítési kulcs hiányzik vagy érvénytelen.", + "error.permission_denied": "A szerver elutasította a bejelentkezést.", + "error.daemon_unreachable": "A NetBird szolgáltatás nem válaszol. Kérjük, ellenőrizze, hogy fut-e a szolgáltatás.", + "error.unknown": "A művelet meghiúsult. A technikai részleteket a Details mezőben találja." } diff --git a/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx b/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx index 7f1f5948f..84e24d077 100644 --- a/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx +++ b/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx @@ -7,6 +7,7 @@ import { ToggleSwitch } from "@/components/ToggleSwitch.tsx"; import { useStatus } from "@/modules/daemon-status/StatusContext.tsx"; import { useProfile } from "@/modules/profile/ProfileContext.tsx"; import { cn } from "@/lib/cn.ts"; +import { formatErrorMessage } from "@/lib/errors.ts"; import netbirdFullLogo from "@/assets/logos/netbird-full.svg"; enum ConnectionState { @@ -38,8 +39,7 @@ const NEEDS_LOGIN_STATES = new Set([ "LoginFailed", ]); -const errorMessage = (e: unknown) => - e instanceof Error ? e.message : String(e); +const errorMessage = formatErrorMessage; // startLogin drives the daemon's SSO login end-to-end. The BrowserLogin // popup window is the only login UI; errors surface as a native diff --git a/client/ui/frontend/src/lib/errors.ts b/client/ui/frontend/src/lib/errors.ts new file mode 100644 index 000000000..2114d8c9c --- /dev/null +++ b/client/ui/frontend/src/lib/errors.ts @@ -0,0 +1,52 @@ +// Shared error formatter for native dialog bodies. +// +// The Go service layer (client/ui/services/connection.go classifyDaemonError) +// wraps daemon errors in a ClientError struct exposed to the TS side as +// {code, short, long}. Short is already localised (Go reads the current +// preferences.Store language and resolves "error." via i18n.Bundle). +// Long always carries the unwrapped raw daemon message so the operator can +// see the JWT / mgm stack when the short text is too generic. +// +// Wails wraps Go-returned errors as Error({message, cause, kind}) where +// .message holds the JSON-stringified payload and the structured object +// lives on .cause — Object.keys(err) is empty in that case. We therefore +// probe .cause first, then fall back to parsing .message as JSON, then +// to plain .message text for callers that still hand us a raw Error. +const extractClientError = (e: unknown): { short?: string; long?: string } | null => { + if (!e || typeof e !== "object") return null; + const withCause = e as { cause?: unknown; message?: unknown }; + if (withCause.cause && typeof withCause.cause === "object") { + return withCause.cause as { short?: string; long?: string }; + } + if (typeof withCause.message === "string") { + const m = withCause.message.trim(); + if (m.startsWith("{") && m.endsWith("}")) { + try { + const parsed = JSON.parse(m); + if (parsed && typeof parsed === "object") { + if ("cause" in parsed && parsed.cause && typeof parsed.cause === "object") { + return parsed.cause as { short?: string; long?: string }; + } + return parsed as { short?: string; long?: string }; + } + } catch { + // not JSON — fall through to plain-message handling + } + } + } + return null; +}; + +export const formatErrorMessage = (e: unknown): string => { + const ce = extractClientError(e); + if (ce) { + const short = typeof ce.short === "string" ? ce.short : ""; + const long = typeof ce.long === "string" ? ce.long : ""; + if (short && long && long !== short) { + return `${short}\n\nDetails: ${long}`; + } + if (short) return short; + } + if (e instanceof Error) return e.message; + return String(e); +}; diff --git a/client/ui/frontend/src/modules/authentication/SessionAboutToExpireDialog.tsx b/client/ui/frontend/src/modules/authentication/SessionAboutToExpireDialog.tsx index 1abe8de02..cf1dd2639 100644 --- a/client/ui/frontend/src/modules/authentication/SessionAboutToExpireDialog.tsx +++ b/client/ui/frontend/src/modules/authentication/SessionAboutToExpireDialog.tsx @@ -16,6 +16,7 @@ import { WindowManager, } from "@bindings/services"; import { useAutoSizeWindow } from "@/lib/useAutoSizeWindow"; +import { formatErrorMessage } from "@/lib/errors.ts"; const DEFAULT_SECONDS = 360; const WINDOW_WIDTH = 360; @@ -63,7 +64,7 @@ export default function SessionAboutToExpireDialog() { if (busy) return; setBusy(true); try { - const start = await Session.RequestExtend({}); + const start = await Session.RequestExtend({ hint: "" }); const uri = start.verificationUriComplete || start.verificationUri; if (uri) { try { @@ -80,7 +81,7 @@ export default function SessionAboutToExpireDialog() { } catch (e) { await Dialogs.Error({ Title: t("sessionAboutToExpire.extendFailedTitle"), - Message: e instanceof Error ? e.message : String(e), + Message: formatErrorMessage(e), }); } finally { setBusy(false); diff --git a/client/ui/frontend/src/modules/debug-bundle/useDebugBundle.ts b/client/ui/frontend/src/modules/debug-bundle/useDebugBundle.ts index fb15ef2c7..d601518ed 100644 --- a/client/ui/frontend/src/modules/debug-bundle/useDebugBundle.ts +++ b/client/ui/frontend/src/modules/debug-bundle/useDebugBundle.ts @@ -6,6 +6,7 @@ import { } from "@bindings/services"; import type { DebugBundleResult } from "@bindings/services/models.js"; import i18next from "@/lib/i18n"; +import { formatErrorMessage } from "@/lib/errors.ts"; import { useProfile } from "@/modules/profile/ProfileContext.tsx"; const NETBIRD_UPLOAD_URL = "https://upload.debug.netbird.io/upload-url"; @@ -158,7 +159,7 @@ export const useDebugBundle = () => { setStage({ kind: "idle" }); await Dialogs.Error({ Title: i18next.t("settings.error.debugBundleTitle"), - Message: e instanceof Error ? e.message : String(e), + Message: formatErrorMessage(e), }); } finally { if (abortRef.current === ctrl) abortRef.current = null; diff --git a/client/ui/frontend/src/modules/settings/LanguagePicker.tsx b/client/ui/frontend/src/modules/settings/LanguagePicker.tsx index a72f60444..b49957e59 100644 --- a/client/ui/frontend/src/modules/settings/LanguagePicker.tsx +++ b/client/ui/frontend/src/modules/settings/LanguagePicker.tsx @@ -11,6 +11,7 @@ import { HelpText } from "@/components/HelpText"; import { Label } from "@/components/Label"; import { loadLanguages } from "@/lib/i18n"; import { cn } from "@/lib/cn"; +import { formatErrorMessage } from "@/lib/errors"; // Flags live alongside the rest of the SVG flag library under // assets/flags/1x1 and are filename-matched to the language code @@ -91,7 +92,7 @@ export function LanguagePicker() { } catch (e) { await Dialogs.Error({ Title: t("settings.error.saveTitle"), - Message: e instanceof Error ? e.message : String(e), + Message: formatErrorMessage(e), }); } finally { setBusy(false); diff --git a/client/ui/frontend/src/modules/settings/SettingsContext.tsx b/client/ui/frontend/src/modules/settings/SettingsContext.tsx index 134fde938..1ba74aad0 100644 --- a/client/ui/frontend/src/modules/settings/SettingsContext.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsContext.tsx @@ -13,9 +13,7 @@ import type { Config } from "@bindings/services/models.js"; import i18next from "@/lib/i18n"; import { useProfile } from "@/modules/profile/ProfileContext.tsx"; import { SkeletonSettings } from "@/modules/skeletons/SkeletonSettings.tsx"; - -const errorMessage = (e: unknown) => - e instanceof Error ? e.message : String(e); +import { formatErrorMessage as errorMessage } from "@/lib/errors.ts"; const SAVE_DEBOUNCE_MS = 400; diff --git a/client/ui/frontend/src/modules/settings/SettingsProfiles.tsx b/client/ui/frontend/src/modules/settings/SettingsProfiles.tsx index 4e91abeda..1b8c57448 100644 --- a/client/ui/frontend/src/modules/settings/SettingsProfiles.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsProfiles.tsx @@ -13,6 +13,7 @@ import i18next from "@/lib/i18n"; import { useProfile } from "@/modules/profile/ProfileContext"; import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; import { cn } from "@/lib/cn"; +import { formatErrorMessage } from "@/lib/errors"; const DEFAULT_PROFILE = "default"; @@ -45,7 +46,7 @@ export function SettingsProfiles() { } catch (e) { await Dialogs.Error({ Title: title, - Message: e instanceof Error ? e.message : String(e), + Message: formatErrorMessage(e), }); } finally { setBusy(false); @@ -90,7 +91,7 @@ export function SettingsProfiles() { } catch (e) { await Dialogs.Error({ Title: i18next.t("profile.error.createTitle"), - Message: e instanceof Error ? e.message : String(e), + Message: formatErrorMessage(e), }); } }; diff --git a/client/ui/frontend/src/screens/Update.tsx b/client/ui/frontend/src/screens/Update.tsx index 4fdc7cbe8..0a09131d4 100644 --- a/client/ui/frontend/src/screens/Update.tsx +++ b/client/ui/frontend/src/screens/Update.tsx @@ -4,6 +4,7 @@ import { Loader2 } from "lucide-react"; import { Dialogs } from "@wailsio/runtime"; import { Update as UpdateSvc } from "@bindings/services"; import i18next from "@/lib/i18n"; +import { formatErrorMessage } from "@/lib/errors"; const TIMEOUT_MS = 15 * 60 * 1000; @@ -20,7 +21,7 @@ export default function Update() { UpdateSvc.Trigger().catch((e) => { if (cancelled) return; setFailed(true); - void showError(e instanceof Error ? e.message : String(e)); + void showError(formatErrorMessage(e)); }); const start = Date.now(); diff --git a/client/ui/main.go b/client/ui/main.go index 03ea30a9a..1c351cdda 100644 --- a/client/ui/main.go +++ b/client/ui/main.go @@ -123,7 +123,6 @@ func main() { }, }) - connection := services.NewConnection(conn) settings := services.NewSettings(conn) profiles := services.NewProfiles(conn) // updater.Holder owns the typed update State. Peers feeds the daemon @@ -133,7 +132,6 @@ func main() { update := services.NewUpdate(conn, updaterHolder) peers := services.NewPeers(conn, app.Event, updaterHolder) notifier := notifications.New() - profileSwitcher := services.NewProfileSwitcher(profiles, connection, peers) // localesFS reroots the embedded tree at the locales directory itself // so the bundle sees _index.json and /common.json at the top @@ -156,6 +154,11 @@ func main() { } localizer := NewLocalizer(bundle, prefStore) + // Connection lives after bundle + prefStore so it can localise daemon + // errors (services.NewConnection takes both as dependencies). + connection := services.NewConnection(conn, bundle, prefStore) + profileSwitcher := services.NewProfileSwitcher(profiles, connection, peers) + app.RegisterService(application.NewService(connection)) // authsession.Session owns the full extend + dismiss surface; the tray // drives the "Extend now" action from the T-10 OS notification through diff --git a/client/ui/services/connection.go b/client/ui/services/connection.go index 97653dc33..4499d3c42 100644 --- a/client/ui/services/connection.go +++ b/client/ui/services/connection.go @@ -4,15 +4,143 @@ package services import ( "context" + "encoding/json" "fmt" "os" "os/exec" "os/user" "runtime" + "strings" + + gstatus "google.golang.org/grpc/status" "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/client/ui/i18n" + "github.com/netbirdio/netbird/client/ui/preferences" ) +// ErrorTranslator is the subset of i18n.Bundle Connection needs to localise +// daemon errors. Defined as an interface so tests can stub it; the runtime +// implementation is *i18n.Bundle. +type ErrorTranslator interface { + Translate(lang i18n.LanguageCode, key string, args ...string) string +} + +// LanguagePreference is the subset of preferences.Store Connection needs +// to discover the current UI language at error-classification time. The +// runtime implementation is *preferences.Store. +type LanguagePreference interface { + Get() preferences.UIPreferences +} + +// ClientError is a structured error returned to the frontend. +// +// The daemon hands us gRPC errors whose Message is a stack of wrapped strings +// from the management server and the underlying JWT library, for example: +// +// "invalid jwt token, err: token could not be parsed: token has invalid +// claims: token used before issued" +// +// Showing that raw message in a native dialog is unreadable, so we map the +// substrings we recognise to a {code, short, long} triple. The frontend +// translates Code through i18n (preferred); Short is an English fallback so +// the dialog still reads cleanly if a code is missing from the locale; Long +// always carries the unwrapped daemon message for the operator. +type ClientError struct { + Code string `json:"code"` + Short string `json:"short"` + Long string `json:"long"` +} + +// Error returns the user-facing short message so plain Go callers and the +// Wails default error path still get a readable string. +func (e *ClientError) Error() string { + if e == nil { + return "" + } + return e.Short +} + +// MarshalJSON encodes the full {code, short, long} triple so the Wails +// binding emits a structured object instead of the default "error: ..." +// string. The TS layer accesses these fields via try/catch. +func (e *ClientError) MarshalJSON() ([]byte, error) { + if e == nil { + return []byte("null"), nil + } + type alias ClientError + return json.Marshal((*alias)(e)) +} + +// classifyDaemonError turns a raw gRPC error from the daemon into a +// ClientError with a stable code and a short localised summary. The Long +// field always carries the unwrapped daemon message so the operator can +// inspect the root cause when the short text is too generic. Short is +// looked up via i18n under "error.": i18n.Bundle.Translate already +// handles current-language → English → key passthrough, so any missing +// locale entry surfaces as a visible "error." string in the dialog — +// a deliberate fail-loud signal that the bundle needs updating. +func (s *Connection) classifyDaemonError(err error) *ClientError { + if err == nil { + return nil + } + + msg := err.Error() + if st, ok := gstatus.FromError(err); ok { + msg = st.Message() + } + lower := strings.ToLower(msg) + + code := "unknown" + switch { + case strings.Contains(lower, "token used before issued"), + strings.Contains(lower, "token is not valid yet"): + code = "jwt_clock_skew" + case strings.Contains(lower, "token is expired"), + strings.Contains(lower, "token has expired"): + code = "jwt_expired" + case strings.Contains(lower, "token signature is invalid"): + code = "jwt_signature_invalid" + case strings.Contains(lower, "peer login has expired"): + code = "session_expired" + case strings.Contains(lower, "invalid setup-key"), + strings.Contains(lower, "invalid setup key"): + code = "invalid_setup_key" + case strings.Contains(lower, "permission denied"): + code = "permission_denied" + case strings.Contains(lower, "no connection could be made"), + strings.Contains(lower, "connection refused"), + strings.Contains(lower, "context deadline exceeded"): + code = "daemon_unreachable" + } + + return &ClientError{ + Code: code, + Short: s.translateShort(code), + Long: msg, + } +} + +// translateShort resolves the localised short message for code. The i18n +// Bundle's own Translate already falls back current-language → English → +// key passthrough, so callers either see the localised string or the bare +// "error." key (which makes the missing translation obvious). If +// the translator is nil — e.g. a Connection constructed in a unit test — +// we return the key for the same reason. +func (s *Connection) translateShort(code string) string { + key := "error." + code + if s.translator == nil { + return key + } + lang := i18n.DefaultLanguage + if s.prefs != nil { + if pref := s.prefs.Get().Language; pref != "" { + lang = pref + } + } + return s.translator.Translate(lang, key) +} + // LoginParams carries the fields the UI sets when starting a login. type LoginParams struct { ProfileName string `json:"profileName"` @@ -52,11 +180,17 @@ type LogoutParams struct { // Connection groups the daemon RPCs that drive login / connect / disconnect. type Connection struct { - conn DaemonConn + conn DaemonConn + translator ErrorTranslator + prefs LanguagePreference } -func NewConnection(conn DaemonConn) *Connection { - return &Connection{conn: conn} +// NewConnection wires Connection with its translation dependencies. Either +// translator or prefs may be nil; in that case classifyDaemonError falls +// back to the English Short text baked into the error map. main.go always +// supplies both at startup. +func NewConnection(conn DaemonConn, translator ErrorTranslator, prefs LanguagePreference) *Connection { + return &Connection{conn: conn, translator: translator, prefs: prefs} } func (s *Connection) Login(ctx context.Context, p LoginParams) (LoginResult, error) { @@ -117,7 +251,7 @@ func (s *Connection) Login(ctx context.Context, p LoginParams) (LoginResult, err resp, err := cli.Login(ctx, req) if err != nil { - return LoginResult{}, err + return LoginResult{}, s.classifyDaemonError(err) } return LoginResult{ NeedsSSOLogin: resp.GetNeedsSSOLogin(), @@ -137,7 +271,7 @@ func (s *Connection) WaitSSOLogin(ctx context.Context, p WaitSSOParams) (string, Hostname: p.Hostname, }) if err != nil { - return "", err + return "", s.classifyDaemonError(err) } return resp.GetEmail(), nil } @@ -155,8 +289,10 @@ func (s *Connection) Up(ctx context.Context, p UpParams) error { if p.Username != "" { req.Username = ptrStr(p.Username) } - _, err = cli.Up(ctx, req) - return err + if _, err = cli.Up(ctx, req); err != nil { + return s.classifyDaemonError(err) + } + return nil } func (s *Connection) Down(ctx context.Context) error { @@ -164,8 +300,10 @@ func (s *Connection) Down(ctx context.Context) error { if err != nil { return err } - _, err = cli.Down(ctx, &proto.DownRequest{}) - return err + if _, err = cli.Down(ctx, &proto.DownRequest{}); err != nil { + return s.classifyDaemonError(err) + } + return nil } // OpenURL launches the user's preferred browser to display url. Mirrors the @@ -201,6 +339,8 @@ func (s *Connection) Logout(ctx context.Context, p LogoutParams) error { if p.Username != "" { req.Username = ptrStr(p.Username) } - _, err = cli.Logout(ctx, req) - return err + if _, err = cli.Logout(ctx, req); err != nil { + return s.classifyDaemonError(err) + } + return nil } diff --git a/go.mod b/go.mod index d86a22e96..e5bbefd71 100644 --- a/go.mod +++ b/go.mod @@ -103,7 +103,7 @@ require ( github.com/ti-mo/conntrack v0.5.1 github.com/ti-mo/netfilter v0.5.2 github.com/vmihailenco/msgpack/v5 v5.4.1 - github.com/wailsapp/wails/v3 v3.0.0-alpha.94 + github.com/wailsapp/wails/v3 v3.0.0-alpha.95 github.com/yusufpapurcu/wmi v1.2.4 github.com/zcalusic/sysinfo v1.1.3 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 @@ -191,7 +191,7 @@ require ( github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect - github.com/go-git/go-git/v5 v5.19.0 // indirect + github.com/go-git/go-git/v5 v5.19.1 // indirect github.com/go-ldap/ldap/v3 v3.4.13 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/go.sum b/go.sum index 4b2fa4dbd..71dcfbea9 100644 --- a/go.sum +++ b/go.sum @@ -194,8 +194,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= -github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= @@ -703,8 +703,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wailsapp/wails/v3 v3.0.0-alpha.94 h1:c/0ZZTj3BFbZQD1s5KHwsshlhunH6YC++gt+cGYV6qA= -github.com/wailsapp/wails/v3 v3.0.0-alpha.94/go.mod h1:4cKvtUppwqYC9tVtvgHWzEmXfUnuLEV3q8d0Jh6xkQQ= +github.com/wailsapp/wails/v3 v3.0.0-alpha.95 h1:Rve8djRSldn6381q2l8gw8XEnzPX/4So6VsRM6bc7Vs= +github.com/wailsapp/wails/v3 v3.0.0-alpha.95/go.mod h1:3euiK0wb6vnXvxiHysRYYbukCa060bLSsfrvN7sZg4k= github.com/wailsapp/wails/webview2 v1.0.24 h1:uULnjCSaRfMlU84mS3kjLgPsRosEOIusVK1nFOHZHzs= github.com/wailsapp/wails/webview2 v1.0.24/go.mod h1:sdf+s0nAdxlzVWf9SCxC15XaxnQPJeY+uU1Ucn3jHQM= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=