From 8b7ce337d86ea5f01ebcf4c62a565ecfa9e7018c Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 19 Jun 2026 09:59:28 +0200 Subject: [PATCH] [client] UI refactor (#6069) Refactor UI --------- Co-authored-by: Eduard Gert Co-authored-by: braginini Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: Claude Co-authored-by: riccardom --- .coderabbit.yaml | 18 + .devcontainer/Dockerfile | 1 - .github/workflows/frontend-ui.yml | 98 + .github/workflows/golang-test-darwin.yml | 10 +- .github/workflows/golang-test-linux.yml | 17 +- .github/workflows/golang-test-windows.yml | 9 +- .github/workflows/golangci-lint.yml | 21 +- .github/workflows/release.yml | 81 +- .github/workflows/wasm-build-validation.yml | 2 +- .golangci.yaml | 10 + .goreleaser.yaml | 2 + .goreleaser_ui.yaml | 26 +- .goreleaser_ui_darwin.yaml | 11 +- CONTRIBUTING.md | 55 +- client/cmd/login.go | 77 + client/cmd/status.go | 7 + client/embed/embed.go | 2 +- client/installer.nsis | 43 +- client/internal/auth/auth.go | 42 + client/internal/auth/auth_test.go | 80 + client/internal/auth/pending_flow.go | 89 + client/internal/auth/sessionwatch/event.go | 82 + client/internal/auth/sessionwatch/watcher.go | 387 ++ .../auth/sessionwatch/watcher_test.go | 519 ++ client/internal/conn_mgr.go | 31 + client/internal/connect.go | 19 +- client/internal/debug/debug.go | 45 +- client/internal/debug/debug_logfiles_test.go | 25 +- client/internal/engine.go | 52 +- client/internal/engine_authsession.go | 108 + client/internal/engine_lazyconn.go | 19 + .../internal/engine_session_deadline_test.go | 78 + client/internal/engine_sessionwatch.go | 16 + client/internal/engine_sessionwatch_js.go | 44 + client/internal/peer/status.go | 199 +- client/internal/peer/status_test.go | 36 + client/internal/profilemanager/state.go | 20 + .../routemanager/exit_node_selection_test.go | 191 + client/internal/routemanager/manager.go | 119 +- .../internal/routeselector/routeselector.go | 4 +- .../routeselector/routeselector_test.go | 28 + client/internal/state.go | 38 +- client/mdm/canonical_loaders.go | 1 + client/mdm/policy.go | 7 + client/netbird.wxs | 43 +- client/proto/daemon.pb.go | 1379 +++-- client/proto/daemon.proto | 117 + client/proto/daemon_grpc.pb.go | 283 +- client/proto/metadata.go | 61 + client/server/debug.go | 23 +- client/server/event.go | 32 + client/server/extend_authsession_test.go | 42 + client/server/mdm.go | 11 +- client/server/network.go | 46 + client/server/network_exitnode_test.go | 26 + client/server/probe_throttle.go | 88 + client/server/probe_throttle_test.go | 109 + client/server/server.go | 485 +- client/server/server_test.go | 26 +- client/server/status_stream.go | 57 + client/ssh/server/test.go | 8 + client/status/status.go | 78 + client/status/status_test.go | 47 + client/ui/.gitignore | 8 + client/ui/Netbird.icns | Bin 80549 -> 0 bytes client/ui/Taskfile.yml | 58 + client/ui/assets/connected.png | Bin 4743 -> 0 bytes client/ui/assets/disconnected.png | Bin 10530 -> 0 bytes client/ui/assets/netbird-disconnected.ico | Bin 5056 -> 0 bytes client/ui/assets/netbird-disconnected.png | Bin 7537 -> 0 bytes client/ui/assets/netbird-menu-16.png | Bin 0 -> 526 bytes client/ui/assets/netbird-menu-24.png | Bin 0 -> 739 bytes client/ui/assets/netbird-menu-about-18.png | Bin 0 -> 838 bytes .../assets/netbird-menu-dot-connected-16.png | Bin 0 -> 508 bytes .../assets/netbird-menu-dot-connected-22.png | Bin 0 -> 615 bytes .../ui/assets/netbird-menu-dot-connected.png | Bin 0 -> 452 bytes .../assets/netbird-menu-dot-connecting-16.png | Bin 0 -> 520 bytes .../assets/netbird-menu-dot-connecting-22.png | Bin 0 -> 637 bytes .../ui/assets/netbird-menu-dot-connecting.png | Bin 0 -> 452 bytes .../ui/assets/netbird-menu-dot-error-16.png | Bin 0 -> 532 bytes .../ui/assets/netbird-menu-dot-error-22.png | Bin 0 -> 629 bytes client/ui/assets/netbird-menu-dot-error.png | Bin 0 -> 433 bytes client/ui/assets/netbird-menu-dot-idle-16.png | Bin 0 -> 490 bytes client/ui/assets/netbird-menu-dot-idle-22.png | Bin 0 -> 602 bytes client/ui/assets/netbird-menu-dot-idle.png | Bin 0 -> 483 bytes .../ui/assets/netbird-menu-dot-offline-16.png | Bin 0 -> 512 bytes .../ui/assets/netbird-menu-dot-offline-22.png | Bin 0 -> 605 bytes client/ui/assets/netbird-menu-dot-offline.png | Bin 0 -> 456 bytes .../netbird-systemtray-connected-dark.ico | Bin 105144 -> 0 bytes .../netbird-systemtray-connected-macos.png | Bin 3858 -> 3690 bytes ...netbird-systemtray-connected-mono-dark.png | Bin 0 -> 1548 bytes .../netbird-systemtray-connected-mono.png | Bin 0 -> 1388 bytes .../assets/netbird-systemtray-connected.ico | Bin 105151 -> 0 bytes .../netbird-systemtray-connecting-dark.ico | Bin 105128 -> 0 bytes .../netbird-systemtray-connecting-macos.png | Bin 3843 -> 3725 bytes ...etbird-systemtray-connecting-mono-dark.png | Bin 0 -> 1448 bytes .../netbird-systemtray-connecting-mono.png | Bin 0 -> 1274 bytes .../assets/netbird-systemtray-connecting.ico | Bin 105091 -> 0 bytes .../netbird-systemtray-disconnected-macos.png | Bin 3491 -> 3474 bytes ...bird-systemtray-disconnected-mono-dark.png | Bin 0 -> 1235 bytes .../netbird-systemtray-disconnected-mono.png | Bin 0 -> 1088 bytes .../netbird-systemtray-disconnected.ico | Bin 104575 -> 0 bytes .../assets/netbird-systemtray-error-dark.ico | Bin 105062 -> 0 bytes .../assets/netbird-systemtray-error-macos.png | Bin 3837 -> 3555 bytes .../netbird-systemtray-error-mono-dark.png | Bin 0 -> 1571 bytes .../assets/netbird-systemtray-error-mono.png | Bin 0 -> 1449 bytes client/ui/assets/netbird-systemtray-error.ico | Bin 105013 -> 0 bytes .../netbird-systemtray-needs-login-macos.png | Bin 0 -> 3555 bytes ...tbird-systemtray-needs-login-mono-dark.png | Bin 0 -> 1571 bytes .../netbird-systemtray-needs-login-mono.png | Bin 0 -> 1449 bytes .../assets/netbird-systemtray-needs-login.png | Bin 0 -> 5260 bytes ...tbird-systemtray-update-connected-dark.ico | Bin 104704 -> 0 bytes ...bird-systemtray-update-connected-macos.png | Bin 3570 -> 3328 bytes ...-systemtray-update-connected-mono-dark.png | Bin 0 -> 1553 bytes ...tbird-systemtray-update-connected-mono.png | Bin 0 -> 1405 bytes .../netbird-systemtray-update-connected.ico | Bin 104698 -> 0 bytes ...rd-systemtray-update-disconnected-dark.ico | Bin 105086 -> 0 bytes ...d-systemtray-update-disconnected-macos.png | Bin 3816 -> 3747 bytes ...stemtray-update-disconnected-mono-dark.png | Bin 0 -> 1444 bytes ...rd-systemtray-update-disconnected-mono.png | Bin 0 -> 1322 bytes ...netbird-systemtray-update-disconnected.ico | Bin 105115 -> 0 bytes client/ui/assets/netbird.ico | Bin 106176 -> 0 bytes client/ui/assets/svg/needs-login.svg | 10 + client/ui/assets/svg/netbird-menu.svg | 7 + client/ui/authsession/service.go | 116 + client/ui/authsession/warning.go | 64 + client/ui/authsession/warning_test.go | 82 + client/ui/build/Taskfile.yml | 295 + .../appicon.icon/Assets/wails_icon_vector.svg | 13 + client/ui/build/appicon.icon/icon.json | 26 + client/ui/build/appicon.png | Bin 0 -> 90556 bytes client/ui/build/build-ui-linux.sh | 5 - client/ui/build/config.yml | 78 + client/ui/build/darwin/Info.dev.plist | 38 + client/ui/build/darwin/Info.plist | 36 + client/ui/build/darwin/Taskfile.yml | 210 + client/ui/build/darwin/icons.icns | Bin 0 -> 141191 bytes client/ui/build/docker/Dockerfile.cross | 203 + client/ui/build/docker/Dockerfile.server | 41 + client/ui/build/linux/Taskfile.yml | 235 + client/ui/build/linux/appimage/build.sh | 35 + client/ui/build/linux/desktop | 13 + client/ui/build/linux/netbird-ui.desktop | 10 + client/ui/build/{ => linux}/netbird.desktop | 3 +- client/ui/build/linux/nfpm/nfpm.yaml | 70 + .../build/linux/nfpm/scripts/postinstall.sh | 21 + .../ui/build/linux/nfpm/scripts/postremove.sh | 1 + .../ui/build/linux/nfpm/scripts/preinstall.sh | 1 + .../ui/build/linux/nfpm/scripts/preremove.sh | 1 + client/ui/build/windows/Taskfile.yml | 243 + client/ui/build/windows/icon.ico | Bin 0 -> 18126 bytes client/ui/build/windows/info.json | 15 + client/ui/build/windows/msix/app_manifest.xml | 55 + client/ui/build/windows/msix/template.xml | 54 + client/ui/build/windows/nsis/project.nsi | 114 + client/ui/build/windows/nsis/wails_tools.nsh | 236 + client/ui/build/windows/wails.exe.manifest | 22 + client/ui/client_ui.go | 1986 ------- client/ui/const.go | 16 - client/ui/debug.go | 730 --- client/ui/dock_darwin.go | 70 + client/ui/dock_other.go | 7 + client/ui/event/event.go | 184 - client/ui/event_handler.go | 326 - client/ui/font_bsd.go | 30 - client/ui/font_darwin.go | 18 - client/ui/font_linux.go | 7 - client/ui/font_windows.go | 90 - client/ui/frontend/.prettierignore | 7 + client/ui/frontend/.prettierrc | 12 + client/ui/frontend/WAILS-API.md | 296 + client/ui/frontend/eslint.config.js | 75 + client/ui/frontend/index.html | 15 + client/ui/frontend/package.json | 69 + client/ui/frontend/pnpm-lock.yaml | 5240 +++++++++++++++++ client/ui/frontend/pnpm-workspace.yaml | 2 + client/ui/frontend/postcss.config.js | 6 + client/ui/frontend/src/app.tsx | 61 + .../src/assets/fonts/inter-variable.ttf | Bin 0 -> 879708 bytes .../assets/fonts/jetbrains-mono-variable.ttf | Bin 0 -> 303144 bytes .../frontend/src/assets/img/tray-darwin.png | Bin 0 -> 196162 bytes .../ui/frontend/src/assets/img/tray-linux.png | Bin 0 -> 177484 bytes .../frontend/src/assets/img/tray-windows.png | Bin 0 -> 159312 bytes .../src/assets/logos/netbird-full.svg | 19 + .../ui/frontend/src/assets/logos/netbird.svg | 5 + client/ui/frontend/src/components/Badge.tsx | 43 + .../src/components/CopyToClipboard.tsx | 126 + .../frontend/src/components/DropdownMenu.tsx | 233 + .../src/components/LanguagePicker.tsx | 235 + .../src/components/ManagementServerSwitch.tsx | 38 + .../ui/frontend/src/components/SquareIcon.tsx | 37 + client/ui/frontend/src/components/Tooltip.tsx | 98 + .../frontend/src/components/TruncatedText.tsx | 32 + .../frontend/src/components/VerticalTabs.tsx | 98 + .../src/components/buttons/Button.tsx | 195 + .../src/components/buttons/IconButton.tsx | 36 + .../src/components/dialog/ConfirmDialog.tsx | 35 + .../src/components/dialog/ConfirmModal.tsx | 84 + .../frontend/src/components/dialog/Dialog.tsx | 159 + .../src/components/dialog/DialogActions.tsx | 13 + .../components/dialog/DialogDescription.tsx | 26 + .../src/components/dialog/DialogHeading.tsx | 35 + .../empty-state/DaemonOutdatedOverlay.tsx | 50 + .../empty-state/DaemonUnavailableOverlay.tsx | 52 + .../src/components/empty-state/EmptyState.tsx | 31 + .../src/components/empty-state/NoResults.tsx | 22 + .../empty-state/NotConnectedState.tsx | 16 + .../frontend/src/components/inputs/Input.tsx | 374 ++ .../src/components/inputs/SearchInput.tsx | 59 + .../components/switches/FancyToggleSwitch.tsx | 102 + .../src/components/switches/SwitchItem.tsx | 42 + .../components/switches/SwitchItemGroup.tsx | 60 + .../src/components/switches/ToggleSwitch.tsx | 77 + .../src/components/typography/HelpText.tsx | 24 + .../src/components/typography/Label.tsx | 42 + .../src/contexts/ClientVersionContext.tsx | 114 + .../src/contexts/DebugBundleContext.tsx | 314 + .../frontend/src/contexts/DialogContext.tsx | 68 + .../src/contexts/NavSectionContext.tsx | 24 + .../frontend/src/contexts/NetworksContext.tsx | 222 + .../src/contexts/PeerDetailContext.tsx | 50 + .../frontend/src/contexts/ProfileContext.tsx | 182 + .../src/contexts/RestrictionsContext.tsx | 65 + .../frontend/src/contexts/SettingsContext.tsx | 267 + .../frontend/src/contexts/StatusContext.tsx | 106 + .../frontend/src/contexts/ViewModeContext.tsx | 89 + client/ui/frontend/src/globals.css | 45 + .../frontend/src/hooks/useAutoSizeWindow.ts | 60 + .../ui/frontend/src/hooks/useFocusVisible.ts | 49 + .../frontend/src/hooks/useKeyboardShortcut.ts | 46 + .../ui/frontend/src/hooks/useManagementUrl.ts | 143 + client/ui/frontend/src/layouts/AppLayout.tsx | 27 + .../ui/frontend/src/layouts/AppRightPanel.tsx | 38 + client/ui/frontend/src/lib/cn.ts | 6 + client/ui/frontend/src/lib/compat.ts | 19 + client/ui/frontend/src/lib/connection.ts | 121 + client/ui/frontend/src/lib/errors.ts | 60 + client/ui/frontend/src/lib/formatters.ts | 44 + client/ui/frontend/src/lib/i18n.ts | 103 + client/ui/frontend/src/lib/logs.ts | 139 + client/ui/frontend/src/lib/platform.ts | 39 + client/ui/frontend/src/lib/sorting.ts | 27 + client/ui/frontend/src/lib/welcome.ts | 25 + .../src/modules/auto-update/UpdateBadge.tsx | 27 + .../auto-update/UpdateInProgressDialog.tsx | 187 + .../modules/auto-update/UpdateVersionCard.tsx | 96 + .../src/modules/error/ErrorDialog.tsx | 64 + .../login/LoginWaitingForBrowserDialog.tsx | 90 + .../main/MainConnectionStatusSwitch.tsx | 410 ++ .../src/modules/main/MainExitNodeSwitcher.tsx | 256 + .../frontend/src/modules/main/MainHeader.tsx | 192 + .../ui/frontend/src/modules/main/MainPage.tsx | 114 + .../src/modules/main/advanced/Navigation.tsx | 134 + .../main/advanced/networks/NetworkFilters.tsx | 84 + .../main/advanced/networks/Networks.tsx | 528 ++ .../main/advanced/peers/PeerDetailPanel.tsx | 539 ++ .../main/advanced/peers/PeerFilters.tsx | 84 + .../src/modules/main/advanced/peers/Peers.tsx | 353 ++ .../src/modules/profiles/ProfileAvatar.tsx | 76 + .../modules/profiles/ProfileCreationModal.tsx | 263 + .../src/modules/profiles/ProfileDropdown.tsx | 293 + .../src/modules/profiles/ProfilesTab.tsx | 679 +++ .../session/SessionExpirationDialog.tsx | 202 + .../src/modules/settings/SettingsAbout.tsx | 169 + .../src/modules/settings/SettingsAccent.tsx | 116 + .../src/modules/settings/SettingsAdvanced.tsx | 179 + .../src/modules/settings/SettingsGeneral.tsx | 113 + .../modules/settings/SettingsNavigation.tsx | 87 + .../src/modules/settings/SettingsNetwork.tsx | 61 + .../src/modules/settings/SettingsPage.tsx | 131 + .../src/modules/settings/SettingsSSH.tsx | 119 + .../src/modules/settings/SettingsSection.tsx | 43 + .../src/modules/settings/SettingsSecurity.tsx | 61 + .../src/modules/settings/SettingsSkeleton.tsx | 27 + .../settings/SettingsTroubleshooting.tsx | 337 ++ .../src/modules/welcome/WelcomeDialog.tsx | 170 + .../modules/welcome/WelcomeStepManagement.tsx | 135 + .../src/modules/welcome/WelcomeStepTray.tsx | 58 + client/ui/frontend/src/vite-env.d.ts | 1 + client/ui/frontend/tailwind.config.ts | 177 + client/ui/frontend/tsconfig.json | 30 + client/ui/frontend/vite.config.ts | 28 + client/ui/grpc.go | 67 + client/ui/guilog/debuglog.go | 77 + client/ui/i18n/TRANSLATING.md | 133 + client/ui/i18n/bundle.go | 196 + client/ui/i18n/bundle_test.go | 156 + client/ui/i18n/locales/_index.json | 13 + client/ui/i18n/locales/de/common.json | 1331 +++++ client/ui/i18n/locales/en/common.json | 1774 ++++++ client/ui/i18n/locales/es/common.json | 1331 +++++ client/ui/i18n/locales/fr/common.json | 1331 +++++ client/ui/i18n/locales/hu/common.json | 1331 +++++ client/ui/i18n/locales/it/common.json | 1331 +++++ client/ui/i18n/locales/pt/common.json | 1331 +++++ client/ui/i18n/locales/ru/common.json | 1331 +++++ client/ui/i18n/locales/zh-CN/common.json | 1331 +++++ client/ui/icons.go | 110 +- client/ui/icons_menu_darwin.go | 23 + client/ui/icons_menu_linux.go | 22 + client/ui/icons_menu_windows.go | 24 + client/ui/icons_windows.go | 44 - client/ui/localizer.go | 130 + client/ui/main.go | 387 ++ client/ui/manifest.xml | 17 - client/ui/netbird-ui.rb.tmpl | 39 - client/ui/network.go | 707 --- client/ui/notifier/notifier.go | 27 - client/ui/notifier/notifier_other.go | 9 - client/ui/notifier/notifier_windows.go | 88 - client/ui/preferences/store.go | 267 + client/ui/preferences/store_test.go | 225 + client/ui/process/process.go | 38 - client/ui/process/process_nonwindows.go | 26 - client/ui/process/process_windows.go | 24 - client/ui/profile.go | 775 --- client/ui/quickactions.go | 349 -- client/ui/quickactions_assets.go | 23 - client/ui/recenter_linux.go | 13 + client/ui/recenter_other.go | 10 + client/ui/services/autostart.go | 52 + client/ui/services/compat.go | 40 + client/ui/services/conn.go | 13 + client/ui/services/connection.go | 223 + client/ui/services/cursor_darwin.go | 42 + client/ui/services/cursor_linux.go | 53 + client/ui/services/cursor_other.go | 9 + client/ui/services/cursor_windows.go | 17 + client/ui/services/daemon_feed.go | 590 ++ client/ui/services/debug.go | 136 + client/ui/services/debug_reveal_other.go | 20 + client/ui/services/debug_reveal_windows.go | 54 + client/ui/services/errors.go | 133 + client/ui/services/errors_test.go | 50 + client/ui/services/foreground_other.go | 11 + client/ui/services/foreground_windows.go | 43 + client/ui/services/forwarding.go | 83 + client/ui/services/i18n.go | 30 + client/ui/services/network.go | 88 + client/ui/services/preferences.go | 36 + client/ui/services/profile.go | 179 + client/ui/services/profileswitcher.go | 92 + client/ui/services/session.go | 48 + client/ui/services/settings.go | 260 + client/ui/services/uilog.go | 36 + client/ui/services/update.go | 73 + client/ui/services/version.go | 22 + client/ui/services/windowmanager.go | 576 ++ client/ui/signal_unix.go | 63 +- client/ui/signal_windows.go | 161 +- client/ui/tray.go | 531 ++ client/ui/tray_click_linux.go | 31 + client/ui/tray_click_other.go | 13 + client/ui/tray_click_windows.go | 8 + client/ui/tray_events.go | 137 + client/ui/tray_exitnodes.go | 148 + client/ui/tray_features.go | 37 + client/ui/tray_icon.go | 136 + client/ui/tray_label_other.go | 7 + client/ui/tray_label_windows.go | 11 + client/ui/tray_linux.go | 71 + client/ui/tray_notify.go | 58 + client/ui/tray_profiles.go | 194 + client/ui/tray_session.go | 271 + client/ui/tray_status.go | 126 + client/ui/tray_status_enabled_linux.go | 8 + client/ui/tray_status_enabled_other.go | 7 + client/ui/tray_status_enabled_windows.go | 7 + client/ui/tray_theme_linux.go | 134 + client/ui/tray_theme_linux_test.go | 86 + client/ui/tray_theme_other.go | 7 + client/ui/tray_theme_watcher_linux.go | 246 + client/ui/tray_update.go | 197 + client/ui/tray_watcher_linux.go | 176 + client/ui/tray_watcher_other.go | 9 + client/ui/uilogpath.go | 37 + client/ui/update.go | 140 - client/ui/update_notwindows.go | 7 - client/ui/update_windows.go | 44 - client/ui/updater/state.go | 97 + client/ui/xembed_host_linux.go | 443 ++ client/ui/xembed_tray_linux.c | 708 +++ client/ui/xembed_tray_linux.h | 81 + docs/io.netbird.client.plist | 7 + docs/netbird-macos.mobileconfig | 2 + docs/netbird-macos.sh | 2 + docs/netbird-policy.reg | Bin 1418 -> 1490 bytes docs/netbird.adml | 3 + docs/netbird.admx | 12 + go.mod | 63 +- go.sum | 136 +- shared/management/client/grpc.go | 26 +- sonar-project.properties | 2 + util/log.go | 55 +- 394 files changed, 46607 insertions(+), 6687 deletions(-) create mode 100644 .coderabbit.yaml create mode 100644 .github/workflows/frontend-ui.yml create mode 100644 client/internal/auth/auth_test.go create mode 100644 client/internal/auth/pending_flow.go create mode 100644 client/internal/auth/sessionwatch/event.go create mode 100644 client/internal/auth/sessionwatch/watcher.go create mode 100644 client/internal/auth/sessionwatch/watcher_test.go create mode 100644 client/internal/engine_authsession.go create mode 100644 client/internal/engine_lazyconn.go create mode 100644 client/internal/engine_session_deadline_test.go create mode 100644 client/internal/engine_sessionwatch.go create mode 100644 client/internal/engine_sessionwatch_js.go create mode 100644 client/internal/routemanager/exit_node_selection_test.go create mode 100644 client/proto/metadata.go create mode 100644 client/server/extend_authsession_test.go create mode 100644 client/server/network_exitnode_test.go create mode 100644 client/server/probe_throttle.go create mode 100644 client/server/probe_throttle_test.go create mode 100644 client/server/status_stream.go create mode 100644 client/ui/.gitignore delete mode 100644 client/ui/Netbird.icns create mode 100644 client/ui/Taskfile.yml delete mode 100644 client/ui/assets/connected.png delete mode 100644 client/ui/assets/disconnected.png delete mode 100644 client/ui/assets/netbird-disconnected.ico delete mode 100644 client/ui/assets/netbird-disconnected.png create mode 100644 client/ui/assets/netbird-menu-16.png create mode 100644 client/ui/assets/netbird-menu-24.png create mode 100644 client/ui/assets/netbird-menu-about-18.png create mode 100644 client/ui/assets/netbird-menu-dot-connected-16.png create mode 100644 client/ui/assets/netbird-menu-dot-connected-22.png create mode 100644 client/ui/assets/netbird-menu-dot-connected.png create mode 100644 client/ui/assets/netbird-menu-dot-connecting-16.png create mode 100644 client/ui/assets/netbird-menu-dot-connecting-22.png create mode 100644 client/ui/assets/netbird-menu-dot-connecting.png create mode 100644 client/ui/assets/netbird-menu-dot-error-16.png create mode 100644 client/ui/assets/netbird-menu-dot-error-22.png create mode 100644 client/ui/assets/netbird-menu-dot-error.png create mode 100644 client/ui/assets/netbird-menu-dot-idle-16.png create mode 100644 client/ui/assets/netbird-menu-dot-idle-22.png create mode 100644 client/ui/assets/netbird-menu-dot-idle.png create mode 100644 client/ui/assets/netbird-menu-dot-offline-16.png create mode 100644 client/ui/assets/netbird-menu-dot-offline-22.png create mode 100644 client/ui/assets/netbird-menu-dot-offline.png delete mode 100644 client/ui/assets/netbird-systemtray-connected-dark.ico create mode 100644 client/ui/assets/netbird-systemtray-connected-mono-dark.png create mode 100644 client/ui/assets/netbird-systemtray-connected-mono.png delete mode 100644 client/ui/assets/netbird-systemtray-connected.ico delete mode 100644 client/ui/assets/netbird-systemtray-connecting-dark.ico create mode 100644 client/ui/assets/netbird-systemtray-connecting-mono-dark.png create mode 100644 client/ui/assets/netbird-systemtray-connecting-mono.png delete mode 100644 client/ui/assets/netbird-systemtray-connecting.ico create mode 100644 client/ui/assets/netbird-systemtray-disconnected-mono-dark.png create mode 100644 client/ui/assets/netbird-systemtray-disconnected-mono.png delete mode 100644 client/ui/assets/netbird-systemtray-disconnected.ico delete mode 100644 client/ui/assets/netbird-systemtray-error-dark.ico create mode 100644 client/ui/assets/netbird-systemtray-error-mono-dark.png create mode 100644 client/ui/assets/netbird-systemtray-error-mono.png delete mode 100644 client/ui/assets/netbird-systemtray-error.ico create mode 100644 client/ui/assets/netbird-systemtray-needs-login-macos.png create mode 100644 client/ui/assets/netbird-systemtray-needs-login-mono-dark.png create mode 100644 client/ui/assets/netbird-systemtray-needs-login-mono.png create mode 100644 client/ui/assets/netbird-systemtray-needs-login.png delete mode 100644 client/ui/assets/netbird-systemtray-update-connected-dark.ico create mode 100644 client/ui/assets/netbird-systemtray-update-connected-mono-dark.png create mode 100644 client/ui/assets/netbird-systemtray-update-connected-mono.png delete mode 100644 client/ui/assets/netbird-systemtray-update-connected.ico delete mode 100644 client/ui/assets/netbird-systemtray-update-disconnected-dark.ico create mode 100644 client/ui/assets/netbird-systemtray-update-disconnected-mono-dark.png create mode 100644 client/ui/assets/netbird-systemtray-update-disconnected-mono.png delete mode 100644 client/ui/assets/netbird-systemtray-update-disconnected.ico delete mode 100644 client/ui/assets/netbird.ico create mode 100644 client/ui/assets/svg/needs-login.svg create mode 100644 client/ui/assets/svg/netbird-menu.svg create mode 100644 client/ui/authsession/service.go create mode 100644 client/ui/authsession/warning.go create mode 100644 client/ui/authsession/warning_test.go create mode 100644 client/ui/build/Taskfile.yml create mode 100644 client/ui/build/appicon.icon/Assets/wails_icon_vector.svg create mode 100644 client/ui/build/appicon.icon/icon.json create mode 100644 client/ui/build/appicon.png delete mode 100644 client/ui/build/build-ui-linux.sh create mode 100644 client/ui/build/config.yml create mode 100644 client/ui/build/darwin/Info.dev.plist create mode 100644 client/ui/build/darwin/Info.plist create mode 100644 client/ui/build/darwin/Taskfile.yml create mode 100644 client/ui/build/darwin/icons.icns create mode 100644 client/ui/build/docker/Dockerfile.cross create mode 100644 client/ui/build/docker/Dockerfile.server create mode 100644 client/ui/build/linux/Taskfile.yml create mode 100644 client/ui/build/linux/appimage/build.sh create mode 100644 client/ui/build/linux/desktop create mode 100755 client/ui/build/linux/netbird-ui.desktop rename client/ui/build/{ => linux}/netbird.desktop (54%) create mode 100644 client/ui/build/linux/nfpm/nfpm.yaml create mode 100644 client/ui/build/linux/nfpm/scripts/postinstall.sh create mode 100644 client/ui/build/linux/nfpm/scripts/postremove.sh create mode 100644 client/ui/build/linux/nfpm/scripts/preinstall.sh create mode 100644 client/ui/build/linux/nfpm/scripts/preremove.sh create mode 100644 client/ui/build/windows/Taskfile.yml create mode 100644 client/ui/build/windows/icon.ico create mode 100644 client/ui/build/windows/info.json create mode 100644 client/ui/build/windows/msix/app_manifest.xml create mode 100644 client/ui/build/windows/msix/template.xml create mode 100644 client/ui/build/windows/nsis/project.nsi create mode 100644 client/ui/build/windows/nsis/wails_tools.nsh create mode 100644 client/ui/build/windows/wails.exe.manifest delete mode 100644 client/ui/client_ui.go delete mode 100644 client/ui/const.go delete mode 100644 client/ui/debug.go create mode 100644 client/ui/dock_darwin.go create mode 100644 client/ui/dock_other.go delete mode 100644 client/ui/event/event.go delete mode 100644 client/ui/event_handler.go delete mode 100644 client/ui/font_bsd.go delete mode 100644 client/ui/font_darwin.go delete mode 100644 client/ui/font_linux.go delete mode 100644 client/ui/font_windows.go create mode 100644 client/ui/frontend/.prettierignore create mode 100644 client/ui/frontend/.prettierrc create mode 100644 client/ui/frontend/WAILS-API.md create mode 100644 client/ui/frontend/eslint.config.js create mode 100644 client/ui/frontend/index.html create mode 100644 client/ui/frontend/package.json create mode 100644 client/ui/frontend/pnpm-lock.yaml create mode 100644 client/ui/frontend/pnpm-workspace.yaml create mode 100644 client/ui/frontend/postcss.config.js create mode 100644 client/ui/frontend/src/app.tsx create mode 100644 client/ui/frontend/src/assets/fonts/inter-variable.ttf create mode 100644 client/ui/frontend/src/assets/fonts/jetbrains-mono-variable.ttf create mode 100644 client/ui/frontend/src/assets/img/tray-darwin.png create mode 100644 client/ui/frontend/src/assets/img/tray-linux.png create mode 100644 client/ui/frontend/src/assets/img/tray-windows.png create mode 100644 client/ui/frontend/src/assets/logos/netbird-full.svg create mode 100644 client/ui/frontend/src/assets/logos/netbird.svg create mode 100644 client/ui/frontend/src/components/Badge.tsx create mode 100644 client/ui/frontend/src/components/CopyToClipboard.tsx create mode 100644 client/ui/frontend/src/components/DropdownMenu.tsx create mode 100644 client/ui/frontend/src/components/LanguagePicker.tsx create mode 100644 client/ui/frontend/src/components/ManagementServerSwitch.tsx create mode 100644 client/ui/frontend/src/components/SquareIcon.tsx create mode 100644 client/ui/frontend/src/components/Tooltip.tsx create mode 100644 client/ui/frontend/src/components/TruncatedText.tsx create mode 100644 client/ui/frontend/src/components/VerticalTabs.tsx create mode 100644 client/ui/frontend/src/components/buttons/Button.tsx create mode 100644 client/ui/frontend/src/components/buttons/IconButton.tsx create mode 100644 client/ui/frontend/src/components/dialog/ConfirmDialog.tsx create mode 100644 client/ui/frontend/src/components/dialog/ConfirmModal.tsx create mode 100644 client/ui/frontend/src/components/dialog/Dialog.tsx create mode 100644 client/ui/frontend/src/components/dialog/DialogActions.tsx create mode 100644 client/ui/frontend/src/components/dialog/DialogDescription.tsx create mode 100644 client/ui/frontend/src/components/dialog/DialogHeading.tsx create mode 100644 client/ui/frontend/src/components/empty-state/DaemonOutdatedOverlay.tsx create mode 100644 client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx create mode 100644 client/ui/frontend/src/components/empty-state/EmptyState.tsx create mode 100644 client/ui/frontend/src/components/empty-state/NoResults.tsx create mode 100644 client/ui/frontend/src/components/empty-state/NotConnectedState.tsx create mode 100644 client/ui/frontend/src/components/inputs/Input.tsx create mode 100644 client/ui/frontend/src/components/inputs/SearchInput.tsx create mode 100644 client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx create mode 100644 client/ui/frontend/src/components/switches/SwitchItem.tsx create mode 100644 client/ui/frontend/src/components/switches/SwitchItemGroup.tsx create mode 100644 client/ui/frontend/src/components/switches/ToggleSwitch.tsx create mode 100644 client/ui/frontend/src/components/typography/HelpText.tsx create mode 100644 client/ui/frontend/src/components/typography/Label.tsx create mode 100644 client/ui/frontend/src/contexts/ClientVersionContext.tsx create mode 100644 client/ui/frontend/src/contexts/DebugBundleContext.tsx create mode 100644 client/ui/frontend/src/contexts/DialogContext.tsx create mode 100644 client/ui/frontend/src/contexts/NavSectionContext.tsx create mode 100644 client/ui/frontend/src/contexts/NetworksContext.tsx create mode 100644 client/ui/frontend/src/contexts/PeerDetailContext.tsx create mode 100644 client/ui/frontend/src/contexts/ProfileContext.tsx create mode 100644 client/ui/frontend/src/contexts/RestrictionsContext.tsx create mode 100644 client/ui/frontend/src/contexts/SettingsContext.tsx create mode 100644 client/ui/frontend/src/contexts/StatusContext.tsx create mode 100644 client/ui/frontend/src/contexts/ViewModeContext.tsx create mode 100644 client/ui/frontend/src/globals.css create mode 100644 client/ui/frontend/src/hooks/useAutoSizeWindow.ts create mode 100644 client/ui/frontend/src/hooks/useFocusVisible.ts create mode 100644 client/ui/frontend/src/hooks/useKeyboardShortcut.ts create mode 100644 client/ui/frontend/src/hooks/useManagementUrl.ts create mode 100644 client/ui/frontend/src/layouts/AppLayout.tsx create mode 100644 client/ui/frontend/src/layouts/AppRightPanel.tsx create mode 100644 client/ui/frontend/src/lib/cn.ts create mode 100644 client/ui/frontend/src/lib/compat.ts create mode 100644 client/ui/frontend/src/lib/connection.ts create mode 100644 client/ui/frontend/src/lib/errors.ts create mode 100644 client/ui/frontend/src/lib/formatters.ts create mode 100644 client/ui/frontend/src/lib/i18n.ts create mode 100644 client/ui/frontend/src/lib/logs.ts create mode 100644 client/ui/frontend/src/lib/platform.ts create mode 100644 client/ui/frontend/src/lib/sorting.ts create mode 100644 client/ui/frontend/src/lib/welcome.ts create mode 100644 client/ui/frontend/src/modules/auto-update/UpdateBadge.tsx create mode 100644 client/ui/frontend/src/modules/auto-update/UpdateInProgressDialog.tsx create mode 100644 client/ui/frontend/src/modules/auto-update/UpdateVersionCard.tsx create mode 100644 client/ui/frontend/src/modules/error/ErrorDialog.tsx create mode 100644 client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx create mode 100644 client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx create mode 100644 client/ui/frontend/src/modules/main/MainExitNodeSwitcher.tsx create mode 100644 client/ui/frontend/src/modules/main/MainHeader.tsx create mode 100644 client/ui/frontend/src/modules/main/MainPage.tsx create mode 100644 client/ui/frontend/src/modules/main/advanced/Navigation.tsx create mode 100644 client/ui/frontend/src/modules/main/advanced/networks/NetworkFilters.tsx create mode 100644 client/ui/frontend/src/modules/main/advanced/networks/Networks.tsx create mode 100644 client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx create mode 100644 client/ui/frontend/src/modules/main/advanced/peers/PeerFilters.tsx create mode 100644 client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx create mode 100644 client/ui/frontend/src/modules/profiles/ProfileAvatar.tsx create mode 100644 client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx create mode 100644 client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx create mode 100644 client/ui/frontend/src/modules/profiles/ProfilesTab.tsx create mode 100644 client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsAbout.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsAccent.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsAdvanced.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsGeneral.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsNavigation.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsNetwork.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsPage.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsSSH.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsSection.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsSecurity.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsSkeleton.tsx create mode 100644 client/ui/frontend/src/modules/settings/SettingsTroubleshooting.tsx create mode 100644 client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx create mode 100644 client/ui/frontend/src/modules/welcome/WelcomeStepManagement.tsx create mode 100644 client/ui/frontend/src/modules/welcome/WelcomeStepTray.tsx create mode 100644 client/ui/frontend/src/vite-env.d.ts create mode 100644 client/ui/frontend/tailwind.config.ts create mode 100644 client/ui/frontend/tsconfig.json create mode 100644 client/ui/frontend/vite.config.ts create mode 100644 client/ui/grpc.go create mode 100644 client/ui/guilog/debuglog.go create mode 100644 client/ui/i18n/TRANSLATING.md create mode 100644 client/ui/i18n/bundle.go create mode 100644 client/ui/i18n/bundle_test.go create mode 100644 client/ui/i18n/locales/_index.json create mode 100644 client/ui/i18n/locales/de/common.json create mode 100644 client/ui/i18n/locales/en/common.json create mode 100644 client/ui/i18n/locales/es/common.json create mode 100644 client/ui/i18n/locales/fr/common.json create mode 100644 client/ui/i18n/locales/hu/common.json create mode 100644 client/ui/i18n/locales/it/common.json create mode 100644 client/ui/i18n/locales/pt/common.json create mode 100644 client/ui/i18n/locales/ru/common.json create mode 100644 client/ui/i18n/locales/zh-CN/common.json create mode 100644 client/ui/icons_menu_darwin.go create mode 100644 client/ui/icons_menu_linux.go create mode 100644 client/ui/icons_menu_windows.go delete mode 100644 client/ui/icons_windows.go create mode 100644 client/ui/localizer.go create mode 100644 client/ui/main.go delete mode 100644 client/ui/manifest.xml delete mode 100644 client/ui/netbird-ui.rb.tmpl delete mode 100644 client/ui/network.go delete mode 100644 client/ui/notifier/notifier.go delete mode 100644 client/ui/notifier/notifier_other.go delete mode 100644 client/ui/notifier/notifier_windows.go create mode 100644 client/ui/preferences/store.go create mode 100644 client/ui/preferences/store_test.go delete mode 100644 client/ui/process/process.go delete mode 100644 client/ui/process/process_nonwindows.go delete mode 100644 client/ui/process/process_windows.go delete mode 100644 client/ui/profile.go delete mode 100644 client/ui/quickactions.go delete mode 100644 client/ui/quickactions_assets.go create mode 100644 client/ui/recenter_linux.go create mode 100644 client/ui/recenter_other.go create mode 100644 client/ui/services/autostart.go create mode 100644 client/ui/services/compat.go create mode 100644 client/ui/services/conn.go create mode 100644 client/ui/services/connection.go create mode 100644 client/ui/services/cursor_darwin.go create mode 100644 client/ui/services/cursor_linux.go create mode 100644 client/ui/services/cursor_other.go create mode 100644 client/ui/services/cursor_windows.go create mode 100644 client/ui/services/daemon_feed.go create mode 100644 client/ui/services/debug.go create mode 100644 client/ui/services/debug_reveal_other.go create mode 100644 client/ui/services/debug_reveal_windows.go create mode 100644 client/ui/services/errors.go create mode 100644 client/ui/services/errors_test.go create mode 100644 client/ui/services/foreground_other.go create mode 100644 client/ui/services/foreground_windows.go create mode 100644 client/ui/services/forwarding.go create mode 100644 client/ui/services/i18n.go create mode 100644 client/ui/services/network.go create mode 100644 client/ui/services/preferences.go create mode 100644 client/ui/services/profile.go create mode 100644 client/ui/services/profileswitcher.go create mode 100644 client/ui/services/session.go create mode 100644 client/ui/services/settings.go create mode 100644 client/ui/services/uilog.go create mode 100644 client/ui/services/update.go create mode 100644 client/ui/services/version.go create mode 100644 client/ui/services/windowmanager.go create mode 100644 client/ui/tray.go create mode 100644 client/ui/tray_click_linux.go create mode 100644 client/ui/tray_click_other.go create mode 100644 client/ui/tray_click_windows.go create mode 100644 client/ui/tray_events.go create mode 100644 client/ui/tray_exitnodes.go create mode 100644 client/ui/tray_features.go create mode 100644 client/ui/tray_icon.go create mode 100644 client/ui/tray_label_other.go create mode 100644 client/ui/tray_label_windows.go create mode 100644 client/ui/tray_linux.go create mode 100644 client/ui/tray_notify.go create mode 100644 client/ui/tray_profiles.go create mode 100644 client/ui/tray_session.go create mode 100644 client/ui/tray_status.go create mode 100644 client/ui/tray_status_enabled_linux.go create mode 100644 client/ui/tray_status_enabled_other.go create mode 100644 client/ui/tray_status_enabled_windows.go create mode 100644 client/ui/tray_theme_linux.go create mode 100644 client/ui/tray_theme_linux_test.go create mode 100644 client/ui/tray_theme_other.go create mode 100644 client/ui/tray_theme_watcher_linux.go create mode 100644 client/ui/tray_update.go create mode 100644 client/ui/tray_watcher_linux.go create mode 100644 client/ui/tray_watcher_other.go create mode 100644 client/ui/uilogpath.go delete mode 100644 client/ui/update.go delete mode 100644 client/ui/update_notwindows.go delete mode 100644 client/ui/update_windows.go create mode 100644 client/ui/updater/state.go create mode 100644 client/ui/xembed_host_linux.go create mode 100644 client/ui/xembed_tray_linux.c create mode 100644 client/ui/xembed_tray_linux.h create mode 100644 sonar-project.properties diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..85ed5cd3b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: en-US +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + auto_review: + enabled: true + drafts: false + path_filters: + - "!**/*.tsx" + - "!**/*.ts" + - "!**/*.js" + - "!**/*.svg" +chat: + auto_reply: true diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 80809e667..0661e0c71 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,7 +6,6 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ iptables=1.8.9-2 \ libgl1-mesa-dev=22.3.6-1+deb12u1 \ xorg-dev=1:7.7+23 \ - libayatana-appindicator3-dev=0.5.92-1 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && go install -v golang.org/x/tools/gopls@latest diff --git a/.github/workflows/frontend-ui.yml b/.github/workflows/frontend-ui.yml new file mode 100644 index 000000000..bb5bb4528 --- /dev/null +++ b/.github/workflows/frontend-ui.yml @@ -0,0 +1,98 @@ +name: UI Frontend + +on: + pull_request: + paths: + - "client/ui/frontend/**" + - "client/ui/i18n/**" + - "client/ui/**/*.go" + - ".github/workflows/frontend-ui.yml" + push: + branches: + - main + paths: + - "client/ui/frontend/**" + - "client/ui/i18n/**" + - "client/ui/**/*.go" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }} + cancel-in-progress: true + +jobs: + lint-and-build: + name: Lint & Build + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: client/ui/frontend + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Set up pnpm + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + with: + version: 11 + + # Bindings are generated by wails3 from the Go service definitions and + # are not checked in (see client/ui/frontend/bindings/). Without them, + # typecheck/build fail on missing module imports. + - name: Set up Go + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: "go.mod" + cache: false + + # wails3 CLI links against GTK4 / WebKitGTK 6.0 via its internal/operatingsystem + # package, so the dev libraries must be present before `go install`. + - name: Install Wails Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + pkg-config \ + libgtk-4-dev \ + libwebkitgtk-6.0-dev + + - name: Install wails3 CLI + # Version derived from go.mod so the binding generator always matches + # the wails runtime the daemon links against. + working-directory: ${{ github.workspace }} + run: | + WAILS_VERSION=$(go list -m -f '{{.Version}}' github.com/wailsapp/wails/v3) + go install github.com/wailsapp/wails/v3/cmd/wails3@$WAILS_VERSION + + - name: Get pnpm store directory + id: pnpm-store + run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('client/ui/frontend/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Wails bindings + run: pnpm run bindings + + - name: Lint, typecheck, format + run: pnpm check + + - name: Build + run: pnpm build diff --git a/.github/workflows/golang-test-darwin.yml b/.github/workflows/golang-test-darwin.yml index ad84840a2..a9e887cf8 100644 --- a/.github/workflows/golang-test-darwin.yml +++ b/.github/workflows/golang-test-darwin.yml @@ -45,7 +45,15 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -coverprofile=coverage.txt -tags=devcert -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined) + # Exclude client/ui: its main.go uses //go:embed all:frontend/dist, + # which fails to compile until the frontend has been built. The Wails UI + # has no Go-side unit tests, and its release pipeline runs `pnpm build` + # before goreleaser. + # `go list -e` lets the listing succeed even though the embed fails to + # resolve; the grep then drops the broken package by path. Without -e, + # go list aborts with empty stdout and `go test` falls back to the repo + # root, which has no Go files. + run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -coverprofile=coverage.txt -tags=devcert -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list -e ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui) - name: Upload coverage reports to Codecov uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1 diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index c17f83222..9222fa922 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -53,7 +53,7 @@ jobs: - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + run: sudo apt update && sudo apt install -y -q libgtk-4-dev libwebkitgtk-6.0-dev libsoup-3.0-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev - name: Install 32-bit libpcap if: steps.cache.outputs.cache-hit != 'true' @@ -145,7 +145,7 @@ jobs: ${{ runner.os }}-gotest-cache- - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + run: sudo apt update && sudo apt install -y -q libgtk-4-dev libwebkitgtk-6.0-dev libsoup-3.0-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev - name: Install 32-bit libpcap if: matrix.arch == '386' @@ -158,7 +158,15 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -coverprofile=coverage.txt -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined) + # Exclude client/ui: its main.go uses //go:embed all:frontend/dist, + # which fails to compile until the frontend has been built. The Wails UI + # has no Go-side unit tests, and its release pipeline runs `pnpm build` + # before goreleaser. + # `go list -e` lets the listing succeed even though the embed fails to + # resolve; the grep then drops the broken package by path. Without -e, + # go list aborts with empty stdout and `go test` falls back to the repo + # root, which has no Go files. + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -coverprofile=coverage.txt -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list -e ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui) - name: Upload coverage reports to Codecov if: matrix.arch == 'amd64' @@ -168,7 +176,6 @@ jobs: slug: netbirdio/netbird flags: unit,client - test_client_on_docker: name: "Client (Docker) / Unit" needs: [build-cache] @@ -229,7 +236,7 @@ jobs: sh -c ' \ apk update; apk add --no-cache \ ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \ - go test -buildvcs=false -tags devcert -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui -e /upload-server) + go test -buildvcs=false -tags devcert -v -timeout 10m -p 1 $(go list -e -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui -e /upload-server) ' test_relay: diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index 8712cc879..c99b8d7c7 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -65,8 +65,15 @@ jobs: - run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }} - run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy - name: Generate test script + # Exclude client/ui: its main.go uses //go:embed all:frontend/dist, + # which fails to compile until the frontend has been built. The Wails UI + # has no Go-side unit tests, and its release pipeline runs `pnpm build` + # before goreleaser. + # `go list -e` lets the listing succeed even though the embed fails to + # resolve; the Where-Object pipeline then drops the broken package by + # path. Without -e, go list aborts with empty stdout. run: | - $packages = go list ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' } + $packages = go list -e ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' } | Where-Object { $_ -notmatch '/client/ui' } $goExe = "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe" $cmd = "$goExe test -tags=devcert -timeout 10m -p 1 $($packages -join ' ') > test-out.txt 2>&1" Set-Content -Path "${{ github.workspace }}\run-tests.cmd" -Value $cmd diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 8f6d1ddb0..e4bbb9cd9 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -22,7 +22,15 @@ jobs: uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 with: ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA,ede,additionals - skip: go.mod,go.sum,**/proxy/web/** + # 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. 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/es/*,*/locales/fr/*,*/locales/hu/*,*/locales/it/*,*/locales/pt/*,*/locales/ru/*,*/locales/zh-CN/*,*/i18n/TRANSLATING.md golangci: strategy: fail-fast: false @@ -54,7 +62,16 @@ jobs: cache: false - name: Install dependencies if: matrix.os == 'ubuntu-latest' - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev + run: sudo apt update && sudo apt install -y -q libgtk-4-dev libwebkitgtk-6.0-dev libsoup-3.0-dev libgl1-mesa-dev xorg-dev libpcap-dev + - name: Stub Wails frontend bundle + # client/ui/main.go has //go:embed all:frontend/dist. The + # directory is produced by `pnpm run build` and is gitignored, so + # lint-only runs (no frontend toolchain) need a placeholder file + # for the embed pattern to match. + shell: bash + run: | + mkdir -p client/ui/frontend/dist + touch client/ui/frontend/dist/.embed-placeholder - name: golangci-lint uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd3514d27..0407f7616 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SIGN_PIPE_VER: "v0.1.6" + SIGN_PIPE_VER: "v0.1.7" GORELEASER_VER: "v2.16.0" PRODUCT_NAME: "NetBird" COPYRIGHT: "NetBird GmbH" @@ -216,9 +216,9 @@ jobs: - name: Install goversioninfo run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e - name: Generate windows syso amd64 - run: goversioninfo -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_amd64.syso + run: goversioninfo -icon client/ui/build/windows/icon.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_amd64.syso - name: Generate windows syso arm64 - run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso + run: goversioninfo -arm -64 -icon client/ui/build/windows/icon.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso - name: Run GoReleaser id: goreleaser uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0 @@ -394,8 +394,18 @@ jobs: - name: check git status run: git --no-pager diff --exit-code + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Set up pnpm + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + with: + version: 11 + - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libappindicator3-dev gir1.2-appindicator3-0.1 libxxf86vm-dev gcc-mingw-w64-x86-64 + run: sudo apt update && sudo apt install -y -q libgtk-4-dev libwebkitgtk-6.0-dev libsoup-3.0-dev gcc-mingw-w64-x86-64 - name: Decode GPG signing key if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository @@ -414,10 +424,16 @@ jobs: echo "/tmp/llvm-mingw-20250709-ucrt-ubuntu-22.04-x86_64/bin" >> $GITHUB_PATH - name: Install goversioninfo run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e + - name: Install wails3 CLI + # Version derived from go.mod so the binding generator always matches + # the wails runtime the binary links against. + run: | + WAILS_VERSION=$(go list -m -f '{{.Version}}' github.com/wailsapp/wails/v3) + go install github.com/wailsapp/wails/v3/cmd/wails3@$WAILS_VERSION - name: Generate windows syso amd64 - run: goversioninfo -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_amd64.syso + run: goversioninfo -64 -icon client/ui/build/windows/icon.ico -manifest client/ui/build/windows/wails.exe.manifest -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_amd64.syso - name: Generate windows syso arm64 - run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso + run: goversioninfo -arm -64 -icon client/ui/build/windows/icon.ico -manifest client/ui/build/windows/wails.exe.manifest -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso - name: Run GoReleaser uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0 @@ -486,6 +502,20 @@ jobs: run: go mod tidy - name: check git status run: git --no-pager diff --exit-code + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Set up pnpm + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + with: + version: 11 + - name: Install wails3 CLI + # Version derived from go.mod so the binding generator always matches + # the wails runtime the binary links against. + run: | + WAILS_VERSION=$(go list -m -f '{{.Version}}' github.com/wailsapp/wails/v3) + go install github.com/wailsapp/wails/v3/cmd/wails3@$WAILS_VERSION - name: Run GoReleaser id: goreleaser uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0 @@ -573,23 +603,6 @@ jobs: - name: Move wintun.dll into dist run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\ - - name: Download Mesa3D (amd64 only) - id: download-mesa3d - if: matrix.arch == 'amd64' - uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2 - with: - url: https://pkgs.netbird.io/mesa3d/MesaForWindows-x64-20.1.8.7z - destination: ${{ env.downloadPath }}\mesa3d.7z - sha256: 71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9 - - - name: Extract Mesa3D driver (amd64 only) - if: matrix.arch == 'amd64' - run: 7z x -o"${{ env.downloadPath }}" "${{ env.downloadPath }}/mesa3d.7z" - - - name: Move opengl32.dll into dist (amd64 only) - if: matrix.arch == 'amd64' - run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\ - - name: Download EnVar plugin for NSIS uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2 with: @@ -612,6 +625,28 @@ jobs: if: matrix.arch == 'amd64' run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z" + - name: Set up Go for wails3 CLI + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + cache: false + + - name: Install wails3 CLI + # Version derived from go.mod so the bootstrapper payload always + # matches the wails runtime the binary links against. + shell: bash + run: | + WAILS_VERSION=$(go list -m -f '{{.Version}}' github.com/wailsapp/wails/v3) + go install github.com/wailsapp/wails/v3/cmd/wails3@$WAILS_VERSION + + - name: Stage WebView2 bootstrapper for installers + # Both client/installer.nsis and client/netbird.wxs reference + # client/MicrosoftEdgeWebview2Setup.exe. wails3 writes it there. + # The signing pipeline (netbirdio/sign-pipelines) does the same + # step for release builds; this mirrors it for PR sanity testing. + shell: bash + run: wails3 generate webview2bootstrapper -dir client + - name: Build NSIS installer shell: pwsh env: diff --git a/.github/workflows/wasm-build-validation.yml b/.github/workflows/wasm-build-validation.yml index 318a127dd..21def2e9d 100644 --- a/.github/workflows/wasm-build-validation.yml +++ b/.github/workflows/wasm-build-validation.yml @@ -27,7 +27,7 @@ jobs: with: go-version-file: "go.mod" - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev + run: sudo apt update && sudo apt install -y -q libgtk-4-dev libwebkitgtk-6.0-dev libsoup-3.0-dev libgl1-mesa-dev xorg-dev libpcap-dev - name: Install golangci-lint uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1 with: diff --git a/.golangci.yaml b/.golangci.yaml index 900af4ac0..e350b9de7 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -114,6 +114,16 @@ linters: - linters: - staticcheck text: "QF1012" + # client/ui/main.go uses //go:embed all:frontend/dist; the + # directory is populated by `pnpm build` in the release pipeline + # and missing at lint time, so the embed parses to "no matching + # files found" — surfaced by golangci-lint's typecheck pre-pass. + # Suppress just that one diagnostic; the rest of the package + # (services/, tray.go, grpc.go, ...) still gets linted normally. + - linters: + - typecheck + path: client/ui/main\.go + text: "pattern all:frontend/dist" paths: - third_party$ - builtin$ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c068f51d1..62f9f1f98 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -212,6 +212,7 @@ nfpms: description: Netbird client. homepage: https://netbird.io/ license: BSD-3-Clause + vendor: NetBird id: netbird_deb bindir: /usr/bin builds: @@ -226,6 +227,7 @@ nfpms: description: Netbird client. homepage: https://netbird.io/ license: BSD-3-Clause + vendor: NetBird id: netbird_rpm bindir: /usr/bin builds: diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index 6f9b7c059..197fcd440 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -2,6 +2,15 @@ version: 2 env: - SKIP_PUBLISH={{ if index .Env "SKIP_PUBLISH" }}{{ .Env.SKIP_PUBLISH }}{{ else }}true{{ end }} project_name: netbird-ui + +before: + hooks: + # Bindings are gitignored; regenerate before the frontend build so + # the @wailsio/runtime Vite plugin can resolve them (vite refuses to + # build without them). + - sh -c 'cd client/ui && wails3 generate bindings -clean=true -ts' + - sh -c 'cd client/ui/frontend && pnpm install --frozen-lockfile && pnpm build' + builds: - id: netbird-ui dir: client/ui @@ -62,6 +71,8 @@ nfpms: - maintainer: Netbird description: Netbird client UI. homepage: https://netbird.io/ + license: BSD-3-Clause + vendor: NetBird id: netbird_ui_deb package_name: netbird-ui builds: @@ -71,9 +82,9 @@ nfpms: scripts: postinstall: "release_files/ui-post-install.sh" contents: - - src: client/ui/build/netbird.desktop - dst: /usr/share/applications/netbird.desktop - - src: client/ui/assets/netbird.png + - src: client/ui/build/linux/netbird.desktop + dst: /usr/share/applications/org.wails.netbird.desktop + - src: client/ui/build/appicon.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird @@ -81,6 +92,8 @@ nfpms: - maintainer: Netbird description: Netbird client UI. homepage: https://netbird.io/ + license: BSD-3-Clause + vendor: NetBird id: netbird_ui_rpm package_name: netbird-ui builds: @@ -90,12 +103,13 @@ nfpms: scripts: postinstall: "release_files/ui-post-install.sh" contents: - - src: client/ui/build/netbird.desktop - dst: /usr/share/applications/netbird.desktop - - src: client/ui/assets/netbird.png + - src: client/ui/build/linux/netbird.desktop + dst: /usr/share/applications/org.wails.netbird.desktop + - src: client/ui/build/appicon.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird + rpm: signature: key_file: '{{ if index .Env "GPG_RPM_KEY_FILE" }}{{ .Env.GPG_RPM_KEY_FILE }}{{ end }}' diff --git a/.goreleaser_ui_darwin.yaml b/.goreleaser_ui_darwin.yaml index 0a0082075..96e15371a 100644 --- a/.goreleaser_ui_darwin.yaml +++ b/.goreleaser_ui_darwin.yaml @@ -1,6 +1,15 @@ version: 2 project_name: netbird-ui + +before: + hooks: + # Bindings are gitignored; regenerate before the frontend build so + # the @wailsio/runtime Vite plugin can resolve them (vite refuses to + # build without them). + - sh -c 'cd client/ui && wails3 generate bindings -clean=true -ts' + - sh -c 'cd client/ui/frontend && pnpm install --frozen-lockfile && pnpm build' + builds: - id: netbird-ui-darwin dir: client/ui @@ -20,8 +29,6 @@ builds: ldflags: - -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser mod_timestamp: "{{ .CommitTimestamp }}" - tags: - - load_wgnt_from_rsrc universal_binaries: - id: netbird-ui-darwin diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd1c087bb..261083783 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,13 +79,21 @@ dependencies are installed. Here is a short guide on how that can be done. ### Requirements -#### Go 1.21 +#### Go 1.25 Follow the installation guide from https://go.dev/ -#### UI client - Fyne toolkit +#### UI client - Wails v3 + React -We use the fyne toolkit in our UI client. You can follow its requirement guide to have all its dependencies installed: https://developer.fyne.io/started/#prerequisites +The desktop UI client (`client/ui`) is built with [Wails v3](https://v3.wails.io/) and a React frontend rendered in a WebView. To build it you need: + +- Go ≥ 1.25 +- Node ≥ 20 and **pnpm** (`corepack enable && corepack prepare pnpm@latest --activate`) +- The `wails3` CLI: `go install github.com/wailsapp/wails/v3/cmd/wails3@latest` +- The `task` runner: `go install github.com/go-task/task/v3/cmd/task@latest` +- Linux only: `libwebkitgtk-6.0-dev`, `libgtk-4-dev`, `libsoup-3.0-dev` + +All UI build, dev-loop, and cross-compile commands are described in the [UI client](#ui-client) section below. #### gRPC You can follow the instructions from the quickstarter guide https://grpc.io/docs/languages/go/quickstart/#prerequisites and then run the `generate.sh` files located in each `proto` directory to generate changes. @@ -214,6 +222,39 @@ To start NetBird the client in the foreground: sudo ./client up --log-level debug --log-file console ``` > On Windows use a powershell with administrator privileges + +#### UI client + +The desktop UI lives in `client/ui` and is built with Wails v3 (see [Requirements](#ui-client---wails-v3--react)). All commands run from `client/ui`. + +Live-reload development (Vite + Go binary + `*.go` watcher): + +``` +cd client/ui +task dev +``` + +Pass daemon flags after `--`: + +``` +task dev -- --daemon-addr=tcp://127.0.0.1:41731 +``` + +Production build (frontend assets embedded into the binary, output in `client/ui/bin/`): + +``` +cd client/ui +task build +``` + +Cross-compile the Windows binary from Linux (requires the mingw-w64 toolchain, e.g. `sudo apt install gcc-mingw-w64-x86-64`): + +``` +CGO_ENABLED=1 task windows:build +``` + +> macOS cross-compile from Linux is not supported (signing and notarization need a real Mac). + #### Signal service To start NetBird's signal, execute: @@ -251,10 +292,10 @@ Create dist directory mkdir -p dist/netbird_windows_amd64 ``` -UI client +UI client (built with Wails v3 — see the [UI client](#ui-client) section above) ```shell -CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -o netbird-ui.exe -ldflags "-s -w -H windowsgui" ./client/ui -mv netbird-ui.exe ./dist/netbird_windows_amd64/ +(cd client/ui && CGO_ENABLED=1 task windows:build) +mv client/ui/bin/netbird-ui.exe ./dist/netbird_windows_amd64/ ``` Client @@ -291,8 +332,6 @@ go test -exec sudo ./... ``` > On Windows use a powershell with administrator privileges -> Non-GTK environments will need the `libayatana-appindicator3-dev` (debian/ubuntu) package installed - ## Checklist before submitting a PR As a critical network service and open-source project, we must enforce a few things before submitting the pull-requests: - Keep functions as simple as possible, with a single purpose diff --git a/client/cmd/login.go b/client/cmd/login.go index 2f7677901..3f3845a5c 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -22,11 +22,19 @@ import ( "github.com/netbirdio/netbird/util" ) +// extendSessionFlag drives the `netbird login --extend` flow: refresh the +// SSO session expiry on the management server without tearing down the +// tunnel. Mutually exclusive with setup-key login (a setup-key cannot +// refresh an SSO-tracked peer — see auth.errSetupKeyOnSSOExpiredPeer). +var extendSessionFlag bool + func init() { loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) loginCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc) loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location") + loginCmd.PersistentFlags().BoolVar(&extendSessionFlag, "extend", false, + "refresh the SSO session expiry without tearing down the tunnel (requires an active connection)") } var loginCmd = &cobra.Command{ @@ -61,6 +69,16 @@ var loginCmd = &cobra.Command{ return err } + if extendSessionFlag { + if providedSetupKey != "" { + return fmt.Errorf("--extend cannot be combined with a setup key; setup keys can only enrol new peers") + } + if err := doExtendSession(ctx, cmd); err != nil { + return fmt.Errorf("extend session failed: %v", err) + } + return nil + } + // workaround to run without service if util.FindFirstLogPath(logFiles) == "" { if err := doForegroundLogin(ctx, cmd, providedSetupKey, activeProf); err != nil { @@ -152,6 +170,65 @@ func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey str return nil } +// doExtendSession drives the daemon's RequestExtendAuthSession / +// WaitExtendAuthSession pair. The user is sent through a regular SSO flow +// (browser + verification URL) and the resulting JWT is forwarded to the +// management server's ExtendAuthSession RPC. The tunnel stays up +// throughout — no Down/Up, no network-map resync. +func doExtendSession(ctx context.Context, cmd *cobra.Command) error { + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + //nolint + return fmt.Errorf("failed to connect to daemon error: %v\n"+ + "If the daemon is not running please run: "+ + "\nnetbird service install \nnetbird service start\n", err) + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + + req := &proto.RequestExtendAuthSessionRequest{} + // Pre-fill the IdP login hint from the active profile so the user + // doesn't have to retype their email. Best-effort: we still proceed + // without a hint if the lookup fails. + pm := profilemanager.NewProfileManager() + if active, perr := pm.GetActiveProfile(); perr == nil { + if profState, sperr := pm.GetProfileState(active.ID); sperr == nil && profState.Email != "" { + req.Hint = &profState.Email + } + } + + startResp, err := client.RequestExtendAuthSession(ctx, req) + if err != nil { + return fmt.Errorf("start extend session: %v", err) + } + + uri := startResp.GetVerificationURIComplete() + if uri == "" { + uri = startResp.GetVerificationURI() + } + openURL(cmd, uri, startResp.GetUserCode(), noBrowser, showQR) + + waitResp, err := client.WaitExtendAuthSession(ctx, &proto.WaitExtendAuthSessionRequest{ + DeviceCode: startResp.GetDeviceCode(), + UserCode: startResp.GetUserCode(), + }) + if err != nil { + return fmt.Errorf("wait for extend session: %v", err) + } + + if ts := waitResp.GetSessionExpiresAt(); ts.IsValid() && !ts.AsTime().IsZero() { + deadline := ts.AsTime().Local() + cmd.Printf("Session extended. New expiry: %s\n", deadline.Format("2006-01-02 15:04:05 MST")) + } else { + // Management reported the peer is not eligible (e.g. login + // expiration disabled on the account). Surface that fact + // instead of pretending the call succeeded. + cmd.Println("Session extension call completed, but the management server did not return a new deadline (peer may not be SSO-tracked or login expiration is disabled).") + } + return nil +} + func getActiveProfile(ctx context.Context, pm *profilemanager.ProfileManager, profileName string, username string) (*profilemanager.Profile, error) { // switch profile if provided diff --git a/client/cmd/status.go b/client/cmd/status.go index 103b3044a..5d8b473f7 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -6,6 +6,7 @@ import ( "net" "net/netip" "strings" + "time" "github.com/spf13/cobra" "google.golang.org/grpc/status" @@ -117,6 +118,11 @@ func statusFunc(cmd *cobra.Command, args []string) error { profName = activeProf.Name } + var sessionExpiresAt time.Time + if ts := resp.GetSessionExpiresAt(); ts.IsValid() { + sessionExpiresAt = ts.AsTime().UTC() + } + var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp.GetFullStatus(), nbstatus.ConvertOptions{ Anonymize: anonymizeFlag, DaemonVersion: resp.GetDaemonVersion(), @@ -127,6 +133,7 @@ func statusFunc(cmd *cobra.Command, args []string) error { IPsFilter: ipsFilterMap, ConnectionTypeFilter: connectionTypeFilter, ProfileName: profName, + SessionExpiresAt: sessionExpiresAt, }) var statusOutputString string switch { diff --git a/client/embed/embed.go b/client/embed/embed.go index 0e8991be2..4233404a3 100644 --- a/client/embed/embed.go +++ b/client/embed/embed.go @@ -468,7 +468,7 @@ func (c *Client) Status() (peer.FullStatus, error) { if connect != nil { engine := connect.Engine() if engine != nil { - _ = engine.RunHealthProbes(false) + _ = engine.RunHealthProbes(context.Background(), false) } } diff --git a/client/installer.nsis b/client/installer.nsis index 63bff1c5b..b64378d08 100644 --- a/client/installer.nsis +++ b/client/installer.nsis @@ -6,7 +6,7 @@ !define DESCRIPTION "Connect your devices into a secure WireGuard-based overlay network with SSO, MFA, and granular access controls." !define INSTALLER_NAME "netbird-installer.exe" !define MAIN_APP_EXE "Netbird" -!define ICON "ui\\assets\\netbird.ico" +!define ICON "ui\\build\\windows\\icon.ico" !define BANNER "ui\\build\\banner.bmp" !define LICENSE_DATA "..\\LICENSE" @@ -280,6 +280,43 @@ CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}" CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}" SectionEnd +# Install the Microsoft Edge WebView2 runtime if it isn't already present. +# Macro adapted from Wails3's NSIS template (wails_tools.nsh): a registry +# probe followed by a silent install of the embedded evergreen bootstrapper. +# The MicrosoftEdgeWebview2Setup.exe payload is staged next to this script +# by the sign-pipelines build step (`wails3 generate webview2bootstrapper`). +!macro nb.webview2runtime + SetRegView 64 + # Per-machine install marker — populated when the runtime ships with + # Edge or has been installed by an admin previously. + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto webview2_ok + ${EndIf} + # Per-user fallback for HKCU installs. + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto webview2_ok + ${EndIf} + + SetDetailsPrint both + DetailPrint "Installing: WebView2 Runtime" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + webview2_ok: +!macroend + +Section -WebView2 + !insertmacro nb.webview2runtime +SectionEnd + Section -Post ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install' ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service start' @@ -326,9 +363,9 @@ DetailPrint "Deleting application files..." Delete "$INSTDIR\${UI_APP_EXE}" Delete "$INSTDIR\${MAIN_APP_EXE}" Delete "$INSTDIR\wintun.dll" -!if ${ARCH} == "amd64" +# Legacy: pre-Wails installs shipped opengl32.dll (Mesa3D for Fyne); remove +# any leftover copy on uninstall so old upgrades don't leave it behind. Delete "$INSTDIR\opengl32.dll" -!endif DetailPrint "Removing application directory..." RmDir /r "$INSTDIR" diff --git a/client/internal/auth/auth.go b/client/internal/auth/auth.go index afc8ee77f..72d63e34e 100644 --- a/client/internal/auth/auth.go +++ b/client/internal/auth/auth.go @@ -3,6 +3,7 @@ package auth import ( "context" "net/url" + "strings" "sync" "time" @@ -21,6 +22,25 @@ import ( mgmProto "github.com/netbirdio/netbird/shared/management/proto" ) +// peerLoginExpiredMsg is the exact phrase the management server returns +// when a previously SSO-enrolled peer's login has expired. Sourced from +// shared/management/status/error.go (NewPeerLoginExpiredError). Matched +// by substring so a future server-side rewording that keeps the phrase +// still triggers the friendly fallback in Login(). +const peerLoginExpiredMsg = "peer login has expired" + +// errSetupKeyOnSSOExpiredPeer replaces the raw management error when the +// user runs `netbird login -k ` against a peer that was +// originally enrolled via SSO. Wrapped in a PermissionDenied gRPC status +// so callers' existing isPermissionDenied / isAuthError checks still +// classify it correctly (early-exit from retry backoff, StatusNeedsLogin +// in the server state machine). +var errSetupKeyOnSSOExpiredPeer = status.Error( + codes.PermissionDenied, + "this peer was originally enrolled via SSO and its session has expired. "+ + "Setup keys can only enrol new peers — run `netbird up` (interactive SSO) to re-login.", +) + // Auth manages authentication operations with the management server // It maintains a long-lived connection and automatically handles reconnection with backoff type Auth struct { @@ -184,6 +204,15 @@ func (a *Auth) Login(ctx context.Context, setupKey string, jwtToken string) (err log.Debugf("peer registration required") _, err = a.registerPeer(client, ctx, setupKey, jwtToken, pubSSHKey) if err != nil { + // The peer pub-key is already on file with the management + // server (originally enrolled via SSO) and the session has + // expired. The setup-key path can only enrol new peers, so + // retrying with -k will keep failing. Replace the raw mgm + // message with an actionable hint that tells the user to + // re-authenticate via SSO instead. + if setupKey != "" && jwtToken == "" && isPeerLoginExpired(err) { + err = errSetupKeyOnSSOExpiredPeer + } isAuthError = isPermissionDenied(err) return err } @@ -474,3 +503,16 @@ func isLoginNeeded(err error) bool { func isRegistrationNeeded(err error) bool { return isPermissionDenied(err) } + +// isPeerLoginExpired reports whether err is the management server's +// "peer login has expired" PermissionDenied response. Used by Login to +// detect the case where the caller passed a setup-key but the peer is +// actually an SSO-enrolled record whose session needs refreshing — the +// setup-key path cannot help there. +func isPeerLoginExpired(err error) bool { + if !isPermissionDenied(err) { + return false + } + s, _ := status.FromError(err) + return strings.Contains(s.Message(), peerLoginExpiredMsg) +} diff --git a/client/internal/auth/auth_test.go b/client/internal/auth/auth_test.go new file mode 100644 index 000000000..e393beccb --- /dev/null +++ b/client/internal/auth/auth_test.go @@ -0,0 +1,80 @@ +package auth + +import ( + "errors" + "strings" + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestIsPeerLoginExpired(t *testing.T) { + cases := []struct { + name string + err error + want bool + }{ + { + name: "nil", + err: nil, + want: false, + }, + { + name: "plain error (not a gRPC status)", + err: errors.New("network read: connection reset"), + want: false, + }, + { + name: "PermissionDenied with different message", + err: status.Error(codes.PermissionDenied, "user is blocked"), + want: false, + }, + { + name: "Unauthenticated with the expected phrase", + // Wrong status code — must still return false. + err: status.Error(codes.Unauthenticated, "peer login has expired, please log in once more"), + want: false, + }, + { + name: "exact server message", + err: status.Error(codes.PermissionDenied, "peer login has expired, please log in once more"), + want: true, + }, + { + name: "phrase as substring", + // Future-proofing: if mgm reworords but keeps the phrase, + // the friendly fallback must still kick in. + err: status.Error(codes.PermissionDenied, "session refused: peer login has expired (account=foo)"), + want: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := isPeerLoginExpired(tc.err); got != tc.want { + t.Fatalf("isPeerLoginExpired(%v) = %v, want %v", tc.err, got, tc.want) + } + }) + } +} + +func TestErrSetupKeyOnSSOExpiredPeer(t *testing.T) { + // Sentinel must surface as PermissionDenied so the upstream + // isPermissionDenied / isAuthError checks classify it correctly + // (short-circuit retry backoff, set StatusNeedsLogin). + if !isPermissionDenied(errSetupKeyOnSSOExpiredPeer) { + t.Fatalf("errSetupKeyOnSSOExpiredPeer must be a PermissionDenied gRPC error") + } + + // Message must actually mention SSO and `netbird up` so it is + // actionable for the end user. Loose substring checks keep the + // test resilient to copy edits. + s, _ := status.FromError(errSetupKeyOnSSOExpiredPeer) + msg := strings.ToLower(s.Message()) + for _, want := range []string{"sso", "netbird up"} { + if !strings.Contains(msg, want) { + t.Errorf("sentinel message should contain %q, got %q", want, s.Message()) + } + } +} diff --git a/client/internal/auth/pending_flow.go b/client/internal/auth/pending_flow.go new file mode 100644 index 000000000..daeb18bc2 --- /dev/null +++ b/client/internal/auth/pending_flow.go @@ -0,0 +1,89 @@ +package auth + +import ( + "context" + "sync" + "time" +) + +// PendingFlow stores an in-progress OAuth flow between the RPC that +// initiates it (returns the verification URI to the UI) and the RPC +// that waits for the user to complete it. The flow handle, the +// device-code info, and the absolute expiry are kept together so the +// waiting RPC can validate the device code and reuse the same flow. +// +// PendingFlow is safe for concurrent use; callers must not access the +// stored fields directly. +type PendingFlow struct { + mu sync.Mutex + flow OAuthFlow + info AuthFlowInfo + expiresAt time.Time + waitCancel context.CancelFunc +} + +// NewPendingFlow returns an empty PendingFlow ready to be populated by Set. +func NewPendingFlow() *PendingFlow { + return &PendingFlow{} +} + +// Set stores the flow and its authorization info, computing the absolute +// expiry from info.ExpiresIn (seconds, as returned by the IdP). +func (p *PendingFlow) Set(flow OAuthFlow, info AuthFlowInfo) { + p.mu.Lock() + defer p.mu.Unlock() + p.flow = flow + p.info = info + p.expiresAt = time.Now().Add(time.Duration(info.ExpiresIn) * time.Second) +} + +// Get returns the stored flow, info, and whether a flow is currently +// pending. Returns (nil, zero, false) after Clear or before Set. +func (p *PendingFlow) Get() (OAuthFlow, AuthFlowInfo, bool) { + p.mu.Lock() + defer p.mu.Unlock() + if p.flow == nil { + return nil, AuthFlowInfo{}, false + } + return p.flow, p.info, true +} + +// ExpiresAt returns the absolute expiry of the pending flow. Returns +// the zero time when no flow is pending. +func (p *PendingFlow) ExpiresAt() time.Time { + p.mu.Lock() + defer p.mu.Unlock() + return p.expiresAt +} + +// SetWaitCancel records the cancel function for the goroutine currently +// blocked in WaitToken so a new RequestAuth can preempt it. +func (p *PendingFlow) SetWaitCancel(cancel context.CancelFunc) { + p.mu.Lock() + defer p.mu.Unlock() + p.waitCancel = cancel +} + +// CancelWait invokes and clears the stored wait-cancel, if any. Safe to +// call when no wait is in progress. +func (p *PendingFlow) CancelWait() { + p.mu.Lock() + cancel := p.waitCancel + p.waitCancel = nil + p.mu.Unlock() + if cancel != nil { + cancel() + } +} + +// Clear resets the pending flow to empty. Any stored wait-cancel is +// dropped without being invoked — call CancelWait first if the waiting +// goroutine must be stopped. +func (p *PendingFlow) Clear() { + p.mu.Lock() + defer p.mu.Unlock() + p.flow = nil + p.info = AuthFlowInfo{} + p.expiresAt = time.Time{} + p.waitCancel = nil +} diff --git a/client/internal/auth/sessionwatch/event.go b/client/internal/auth/sessionwatch/event.go new file mode 100644 index 000000000..3e55b26dd --- /dev/null +++ b/client/internal/auth/sessionwatch/event.go @@ -0,0 +1,82 @@ +package sessionwatch + +import ( + "strconv" + "time" +) + +// internal event kinds are no longer exposed: the watcher drives the Sink +// directly (NotifyStateChange on deadline change/clear, PublishEvent at +// each warning lead). Tests use a mock Sink to observe what the watcher +// emits. + +// Metadata keys attached by the daemon to session-warning SystemEvents. +// The UI tray reads these to build a locale-aware notification without +// relying on the daemon's locale-less UserMessage string, and to +// disambiguate the T-WarningLead notification from the T-FinalWarningLead +// fallback that auto-opens the SessionAboutToExpire dialog. +const ( + // MetaSessionWarning is set to "true" on both warning events (T-10 and + // T-2) so the UI can detect a session-warning SystemEvent without + // matching on the message text. Use MetaSessionFinal to distinguish + // the two. + MetaSessionWarning = "session_warning" + // MetaSessionFinal is set to "true" on the T-FinalWarningLead event + // only. Consumers that need to auto-open the SessionAboutToExpire + // dialog gate on this; T-WarningLead events leave the field unset. + MetaSessionFinal = "session_final_warning" + // MetaSessionExpiresAt carries the absolute UTC deadline encoded with + // FormatExpiresAt; consumers must decode with ParseExpiresAt so a + // future format change stays a single edit. + MetaSessionExpiresAt = "session_expires_at" + // MetaSessionLeadMinutes carries the lead in whole minutes (WarningLead + // 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. +// Producer and consumers both go through FormatExpiresAt/ParseExpiresAt +// so this layout stays a single source of truth. +const expiresAtLayout = time.RFC3339 + +// FormatExpiresAt encodes a deadline for MetaSessionExpiresAt. Always +// emits UTC so a consumer in another timezone reads the same wall-clock +// deadline. +func FormatExpiresAt(t time.Time) string { + return t.UTC().Format(expiresAtLayout) +} + +// ParseExpiresAt decodes the MetaSessionExpiresAt value back to a UTC +// time. Returns an error when the field is empty or malformed; the +// caller decides whether to fall back (zero value) or propagate. +func ParseExpiresAt(s string) (time.Time, error) { + t, err := time.Parse(expiresAtLayout, s) + if err != nil { + return time.Time{}, err + } + return t.UTC(), nil +} + +// FormatLeadMinutes encodes a lead duration for MetaSessionLeadMinutes +// as the integer count of whole minutes. Sub-minute residuals are +// truncated — the field is informational ("expires in ~N minutes") and +// fractional minutes don't change what the UI displays. +func FormatLeadMinutes(d time.Duration) string { + return strconv.Itoa(int(d / time.Minute)) +} + +// ParseLeadMinutes decodes a MetaSessionLeadMinutes value. Returns 0 +// and the parse error for malformed input; consumers that prefer a +// silent fallback can simply ignore the error. +func ParseLeadMinutes(s string) (int, error) { + return strconv.Atoi(s) +} diff --git a/client/internal/auth/sessionwatch/watcher.go b/client/internal/auth/sessionwatch/watcher.go new file mode 100644 index 000000000..e75a7022e --- /dev/null +++ b/client/internal/auth/sessionwatch/watcher.go @@ -0,0 +1,387 @@ +// Package sessionwatch tracks the SSO session expiry deadline that the +// management server publishes via LoginResponse / SyncResponse and fires +// two warning events at fixed lead times before expiry: an interactive +// T-WarningLead notification and a dismiss-gated T-FinalWarningLead +// fallback dialog. +// +// The watcher is idempotent: Update may be called as often as the network +// map snapshots arrive. Repeating the same deadline is a no-op; a new +// deadline reschedules the timers and arms a fresh warning cycle. +// +// Warning firing is edge-detected. Each unique deadline value fires each +// warning callback at most once. +package sessionwatch + +import ( + "errors" + "fmt" + "sync" + "time" + + log "github.com/sirupsen/logrus" + + cProto "github.com/netbirdio/netbird/client/proto" +) + +const ( + // Skew tolerates a small clock difference between the management + // server and this peer before treating a deadline as "in the past". + // Slightly above typical NTP drift; tight enough that the UI doesn't + // paint a stale expiry as if it were valid. + Skew = 30 * time.Second + + // maxDeadlineHorizon caps how far in the future an accepted deadline + // can sit. A timestamp beyond this is almost certainly a protocol + // glitch, and silently arming a 100-year timer would hide the bug. + maxDeadlineHorizon = 10 * 365 * 24 * time.Hour + + // WarningLead is how far before expiry the first (interactive) + // warning fires. Drives the T-10 OS notification with + // Extend/Dismiss actions. + WarningLead = 10 * time.Minute + + // FinalWarningLead is how far before expiry the fallback final + // warning fires. Drives the auto-opened SessionAboutToExpire dialog, + // but only when the user has not dismissed the T-WarningLead warning + // for the same deadline. Must be strictly less than WarningLead. + FinalWarningLead = 2 * time.Minute +) + +var ( + // ErrDeadlineBeforeEpoch is returned by Update when the supplied + // deadline pre-dates 1970-01-01. + ErrDeadlineBeforeEpoch = errors.New("session deadline before unix epoch") + + // ErrDeadlineTooFarFuture is returned by Update when the supplied + // deadline is more than maxDeadlineHorizon in the future. + ErrDeadlineTooFarFuture = errors.New("session deadline too far in the future") + + // ErrDeadlineInPast is returned by Update when the supplied deadline + // is more than Skew in the past. + ErrDeadlineInPast = errors.New("session deadline in the past") +) + +// StatusRecorder is the side-effect surface the watcher drives on every +// state transition. Production wires this to peer.Status (SetSessionExpiresAt +// for deadline change/clear, PublishEvent for the two warnings); tests pass +// a fake recorder so the same surface is observable without an engine. +// +// The watcher is the single owner of the deadline propagated to the +// recorder: every set, clear, sanity-check rejection and Close routes the +// value through SetSessionExpiresAt, so the SubscribeStatus snapshot the UI +// reads can never drift from the watcher's timer state. (SetSessionExpiresAt +// fans out its own state-change notification, so no separate notify is +// needed.) The recorder is server-scoped and outlives this engine-scoped +// watcher — without the Close-time clear a teardown (Down, or the Down+Up of +// a profile switch) would leave the next session showing the previous one's +// stale "expires in" value. +// +// PublishEvent's signature mirrors peer.Status.PublishEvent: the watcher +// composes the metadata internally so the wire format (MetaSession*) is +// owned by sessionwatch, not the caller. +type StatusRecorder interface { + SetSessionExpiresAt(deadline time.Time) + PublishEvent( + severity cProto.SystemEvent_Severity, + category cProto.SystemEvent_Category, + message string, + userMessage string, + metadata map[string]string, + ) +} + +// Watcher observes the latest session deadline and fires two warnings +// before it expires: the interactive T-WarningLead notification, and the +// fallback T-FinalWarningLead dialog (suppressed when the user dismissed +// the first one for the same deadline). Safe for concurrent use. +type Watcher struct { + lead time.Duration + finalLead time.Duration + + mu sync.Mutex + current time.Time + timer *time.Timer + finalTimer *time.Timer + firedAt time.Time // deadline value the T-WarningLead callback last fired against + finalFiredAt time.Time // deadline value the T-FinalWarningLead callback last fired against + dismissedAt time.Time // deadline value the user dismissed via Dismiss(); gates fireFinal + closed bool + recorder StatusRecorder +} + +// New returns a watcher with the package defaults WarningLead and +// FinalWarningLead. Pass nil for recorder to silence side effects (handy +// in unit tests that exercise sanity checks without observing the publish +// path). +func New(recorder StatusRecorder) *Watcher { + return NewWithLeads(WarningLead, FinalWarningLead, recorder) +} + +// NewWithLeads returns a watcher with custom lead times. Useful for tests. +// final must be strictly less than lead; otherwise both timers fire in the +// wrong order or simultaneously and the UI flow breaks. A zero final lead +// disables the final-warning timer entirely (see armTimerLocked) so a +// millisecond-scale deadline doesn't flush both timers in one tick. +func NewWithLeads(lead, final time.Duration, recorder StatusRecorder) *Watcher { + return &Watcher{ + lead: lead, + finalLead: final, + recorder: recorder, + } +} + +// Update sets the latest deadline. Pass the zero time to clear (e.g. when +// a Sync push from the server omits the field because login expiration +// was disabled). +// +// Same-value updates are no-ops. A different non-zero value cancels any +// pending timer, resets the "already fired" guard, and arms a new one. +// +// Returns one of the sentinel Err* values when the deadline fails the +// sanity checks (pre-epoch, far future, or in the past beyond Skew). +// In every error case the watcher first clears its state so it stays +// consistent with what the caller will push into its other sinks (e.g. +// applySessionDeadline forces a zero deadline into the status recorder +// after a non-nil error). +func (w *Watcher) Update(deadline time.Time) error { + w.mu.Lock() + if w.closed { + w.mu.Unlock() + return nil + } + + if deadline.IsZero() { + w.clearLocked() + return nil + } + + now := time.Now() + switch { + case deadline.Before(time.Unix(0, 0)): + w.clearLocked() + return fmt.Errorf("%w: %v", ErrDeadlineBeforeEpoch, deadline) + case deadline.After(now.Add(maxDeadlineHorizon)): + w.clearLocked() + return fmt.Errorf("%w: %v", ErrDeadlineTooFarFuture, deadline) + case deadline.Before(now.Add(-Skew)): + w.clearLocked() + return fmt.Errorf("%w: %v (now=%v)", ErrDeadlineInPast, deadline, now) + } + + if deadline.Equal(w.current) { + w.mu.Unlock() + return nil + } + + w.stopTimerLocked() + w.current = deadline + // Reset every per-deadline guard so a refreshed deadline arms a fresh + // warning cycle: both edge triggers and the user Dismiss decision + // (the user agreed to the old deadline expiring; a new deadline + // restarts the contract). + w.firedAt = time.Time{} + w.finalFiredAt = time.Time{} + w.dismissedAt = time.Time{} + + w.armTimerLocked(deadline) + recorder := w.recorder + w.mu.Unlock() + if recorder != nil { + recorder.SetSessionExpiresAt(deadline) + } + log.Infof("auth session deadline set to: %s (in %s)", deadline.Format(time.RFC3339), time.Until(deadline).Round(time.Second)) + return nil +} + +// Deadline returns the most recently observed deadline. Zero when no +// deadline is currently tracked. +func (w *Watcher) Deadline() time.Time { + w.mu.Lock() + defer w.mu.Unlock() + return w.current +} + +// Dismiss records the user's "Dismiss" action against the current deadline +// and suppresses the upcoming final-warning callback for that deadline. +// Idempotent: repeated calls are no-ops. A subsequent Update with a fresh +// deadline resets the dismissal so the final-warning cycle re-arms. +// +// No-op when the watcher holds no deadline or has been closed. +func (w *Watcher) Dismiss() { + w.mu.Lock() + defer w.mu.Unlock() + if w.closed || w.current.IsZero() { + return + } + if w.dismissedAt.Equal(w.current) { + return + } + w.dismissedAt = w.current + // Cancel the armed final-warning timer eagerly. fireFinal would also + // gate on dismissedAt, but stopping the timer avoids a wakeup with + // nothing to do and makes the intent visible. + if w.finalTimer != nil { + w.finalTimer.Stop() + w.finalTimer = nil + } + log.Infof("auth session final-warning dismissed for deadline %s", w.current.Format(time.RFC3339)) +} + +// Close stops any pending timer and drops the deadline on the status +// recorder. Update calls after Close are ignored. Clearing the recorder +// here is what keeps a teardown (Down, or the Down+Up of a profile switch) +// from leaving the next session showing this one's stale "expires in" +// value — the recorder is server-scoped and outlives this engine-scoped +// watcher, so nothing else drops the anchor on teardown. +func (w *Watcher) Close() { + w.mu.Lock() + if w.closed { + w.mu.Unlock() + return + } + w.closed = true + w.stopTimerLocked() + hadDeadline := !w.current.IsZero() + w.current = time.Time{} + w.firedAt = time.Time{} + w.finalFiredAt = time.Time{} + w.dismissedAt = time.Time{} + recorder := w.recorder + w.mu.Unlock() + if recorder != nil && hadDeadline { + recorder.SetSessionExpiresAt(time.Time{}) + } +} + +// clearLocked drops the tracked deadline and notifies the recorder so +// downstream consumers (SubscribeStatus stream, UI) drop their anchor. +// The caller must hold w.mu; this helper releases it before invoking +// the recorder. +func (w *Watcher) clearLocked() { + if w.current.IsZero() { + w.mu.Unlock() + return + } + w.stopTimerLocked() + w.current = time.Time{} + w.firedAt = time.Time{} + w.finalFiredAt = time.Time{} + w.dismissedAt = time.Time{} + recorder := w.recorder + w.mu.Unlock() + if recorder != nil { + recorder.SetSessionExpiresAt(time.Time{}) + } + log.Infof("auth session deadline cleared") +} + +func (w *Watcher) stopTimerLocked() { + if w.timer != nil { + w.timer.Stop() + w.timer = nil + } + if w.finalTimer != nil { + w.finalTimer.Stop() + w.finalTimer = nil + } +} + +func (w *Watcher) armTimerLocked(deadline time.Time) { + w.timer = armOneShotLocked(deadline.Add(-w.lead), func() { w.fire(deadline) }) + // finalLead <= 0 disables the final-warning timer entirely. Used by + // tests that predate the final-warning fallback so a millisecond-scale + // deadline does not flush both timers at once. + if w.finalLead > 0 { + w.finalTimer = armOneShotLocked(deadline.Add(-w.finalLead), func() { w.fireFinal(deadline) }) + } +} + +func (w *Watcher) fire(armedFor time.Time) { + w.mu.Lock() + if w.closed || !w.current.Equal(armedFor) { + // Deadline moved while we were waiting (e.g. a successful extend). + // The reschedule path armed a fresh timer; this one is stale. + w.mu.Unlock() + return + } + if !w.firedAt.IsZero() && w.firedAt.Equal(armedFor) { + w.mu.Unlock() + return + } + w.firedAt = armedFor + recorder := w.recorder + w.mu.Unlock() + if recorder == nil { + return + } + log.Infof("auth session expiry soon warning fired") + publishWarning(recorder, armedFor, false) +} + +// fireFinal mirrors fire for the T-FinalWarningLead timer with an extra +// dismiss-gate: if the user dismissed the T-WarningLead notification for +// this deadline, the final warning is suppressed entirely. +func (w *Watcher) fireFinal(armedFor time.Time) { + w.mu.Lock() + if w.closed || !w.current.Equal(armedFor) { + w.mu.Unlock() + return + } + if !w.finalFiredAt.IsZero() && w.finalFiredAt.Equal(armedFor) { + w.mu.Unlock() + return + } + if w.dismissedAt.Equal(armedFor) { + w.mu.Unlock() + log.Infof("auth session final-warning skipped (dismissed by user)") + return + } + w.finalFiredAt = armedFor + recorder := w.recorder + w.mu.Unlock() + if recorder == nil { + return + } + log.Infof("auth session final-warning fired") + publishWarning(recorder, armedFor, true) +} + +// armOneShotLocked schedules cb at fireAt. When fireAt is already in the +// past it dispatches on the next scheduler tick so a state-change recorder +// notification (invoked after w.mu is released) lands first. Caller must +// hold w.mu. +func armOneShotLocked(fireAt time.Time, cb func()) *time.Timer { + delay := time.Until(fireAt) + if delay <= 0 { + return time.AfterFunc(0, cb) + } + return time.AfterFunc(delay, cb) +} + +// publishWarning composes the SystemEvent for a watcher-fired warning and +// pushes it through the recorder. Severity is CRITICAL on both — bypassing +// the user's Notifications toggle is deliberate: missing the warning +// window forces the post-mortem SessionExpired flow (tunnel torn down, +// lock icon, manual re-login), which is the UX we are trying to avoid. +func publishWarning(recorder StatusRecorder, deadline time.Time, final bool) { + lead := WarningLead + message := "session expiry warning" + meta := map[string]string{ + MetaSessionWarning: "true", + MetaSessionExpiresAt: FormatExpiresAt(deadline), + } + if final { + lead = FinalWarningLead + message = "session expiry final warning" + meta[MetaSessionFinal] = "true" + } + meta[MetaSessionLeadMinutes] = FormatLeadMinutes(lead) + + recorder.PublishEvent( + cProto.SystemEvent_CRITICAL, + cProto.SystemEvent_AUTHENTICATION, + message, + "", + meta, + ) +} diff --git a/client/internal/auth/sessionwatch/watcher_test.go b/client/internal/auth/sessionwatch/watcher_test.go new file mode 100644 index 000000000..da2b6add6 --- /dev/null +++ b/client/internal/auth/sessionwatch/watcher_test.go @@ -0,0 +1,519 @@ +package sessionwatch + +import ( + "errors" + "sync" + "testing" + "time" + + cProto "github.com/netbirdio/netbird/client/proto" +) + +// fakeRecorder satisfies StatusRecorder and records every call so tests +// can observe what the watcher emits. SetSessionExpiresAt and PublishEvent +// land in the same ordered events slice (with the Kind distinguishing +// them) so tests that care about ordering still work. lastDeadline holds +// the most recent value passed to SetSessionExpiresAt so tests can assert +// the recorder ended up cleared/set as expected. +type fakeRecorder struct { + mu sync.Mutex + events []event + lastDeadline time.Time +} + +type eventKind int + +const ( + stateChange eventKind = iota + publish +) + +type event struct { + kind eventKind + // Set only for publish events. + severity cProto.SystemEvent_Severity + category cProto.SystemEvent_Category + message string + meta map[string]string +} + +// SetSessionExpiresAt mirrors peer.Status: a same-value write is a no-op, +// a real change records the new value and fans out a state-change (the +// production recorder calls notifyStateChange internally). The baseline +// is the zero time, so an initial clear before any deadline is set emits +// nothing — matching the real recorder. +func (r *fakeRecorder) SetSessionExpiresAt(deadline time.Time) { + r.mu.Lock() + defer r.mu.Unlock() + if r.lastDeadline.Equal(deadline) { + return + } + r.lastDeadline = deadline + r.events = append(r.events, event{kind: stateChange}) +} + +func (r *fakeRecorder) deadline() time.Time { + r.mu.Lock() + defer r.mu.Unlock() + return r.lastDeadline +} + +func (r *fakeRecorder) PublishEvent( + severity cProto.SystemEvent_Severity, + category cProto.SystemEvent_Category, + message string, + _ string, + metadata map[string]string, +) { + r.mu.Lock() + defer r.mu.Unlock() + r.events = append(r.events, event{ + kind: publish, + severity: severity, + category: category, + message: message, + meta: metadata, + }) +} + +func (r *fakeRecorder) snapshot() []event { + r.mu.Lock() + defer r.mu.Unlock() + out := make([]event, len(r.events)) + copy(out, r.events) + return out +} + +func (e event) isFinalWarning() bool { + return e.kind == publish && e.meta[MetaSessionFinal] == "true" +} + +func (e event) isWarning() bool { + return e.kind == publish && e.meta[MetaSessionWarning] == "true" && e.meta[MetaSessionFinal] != "true" +} + +func countWhere(events []event, pred func(event) bool) int { + n := 0 + for _, e := range events { + if pred(e) { + n++ + } + } + return n +} + +func waitForEvents(t *testing.T, r *fakeRecorder, want int) []event { + t.Helper() + deadline := time.Now().Add(500 * time.Millisecond) + for time.Now().Before(deadline) { + if got := r.snapshot(); len(got) >= want { + return got + } + time.Sleep(5 * time.Millisecond) + } + got := r.snapshot() + t.Fatalf("timed out waiting for %d events, got %d: %+v", want, len(got), got) + return nil +} + +// newWatcher builds a watcher with the final timer disabled (finalLead=0), +// matching the lead-only behaviour the pre-final-warning tests assume. +func newWatcher(lead time.Duration, r *fakeRecorder) *Watcher { + return NewWithLeads(lead, 0, r) +} + +func TestUpdateZeroBeforeAnythingIsNoop(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(50*time.Millisecond, r) + defer w.Close() + + _ = w.Update(time.Time{}) + + if got := r.snapshot(); len(got) != 0 { + t.Fatalf("expected no events on initial zero, got %+v", got) + } +} + +func TestUpdateNonZeroFiresStateChange(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(50*time.Millisecond, r) + defer w.Close() + + d := time.Now().Add(time.Hour) + _ = w.Update(d) + + events := waitForEvents(t, r, 1) + if events[0].kind != stateChange { + t.Fatalf("expected stateChange, got %+v", events[0]) + } + if !w.Deadline().Equal(d) { + t.Fatalf("deadline mismatch: %v vs %v", w.Deadline(), d) + } +} + +func TestSameDeadlineIsNoop(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(50*time.Millisecond, r) + defer w.Close() + + d := time.Now().Add(time.Hour) + _ = w.Update(d) + _ = w.Update(d) + _ = w.Update(d) + + events := waitForEvents(t, r, 1) + if len(events) != 1 { + t.Fatalf("expected exactly 1 event for repeated same deadline, got %d: %+v", len(events), events) + } +} + +func TestWarningFiresOnceWithinLeadWindow(t *testing.T) { + r := &fakeRecorder{} + lead := 50 * time.Millisecond + w := newWatcher(lead, r) + defer w.Close() + + // Deadline 80ms out — warning should fire after ~30ms. + d := time.Now().Add(80 * time.Millisecond) + _ = w.Update(d) + + events := waitForEvents(t, r, 2) + if events[0].kind != stateChange { + t.Fatalf("event[0] should be stateChange, got %+v", events[0]) + } + if !events[1].isWarning() { + t.Fatalf("event[1] should be a warning publish, got %+v", events[1]) + } +} + +func TestWarningFiresImmediatelyWhenAlreadyInsideWindow(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(time.Hour, r) // lead > delta => fire immediately + defer w.Close() + + d := time.Now().Add(10 * time.Millisecond) + _ = w.Update(d) + + events := waitForEvents(t, r, 2) + if !events[1].isWarning() { + t.Fatalf("expected immediate warning publish, got %+v", events[1]) + } +} + +func TestNewDeadlineCancelsPriorTimer(t *testing.T) { + r := &fakeRecorder{} + lead := 50 * time.Millisecond + w := newWatcher(lead, r) + defer w.Close() + + first := time.Now().Add(80 * time.Millisecond) // would fire warning ~30ms in + _ = w.Update(first) + + // Replace with a far-future deadline before the warning fires. + time.Sleep(5 * time.Millisecond) + second := time.Now().Add(time.Hour) + _ = w.Update(second) + + // Wait past when first's warning would have fired. + time.Sleep(80 * time.Millisecond) + + if n := countWhere(r.snapshot(), event.isWarning); n != 0 { + t.Fatalf("warning fired for cancelled deadline: %+v", r.snapshot()) + } +} + +func TestRefreshAfterFireArmsNewWarning(t *testing.T) { + r := &fakeRecorder{} + lead := 30 * time.Millisecond + w := newWatcher(lead, r) + defer w.Close() + + first := time.Now().Add(50 * time.Millisecond) + _ = w.Update(first) + + // Wait for stateChange + warning of the first cycle. + waitForEvents(t, r, 2) + + // Simulate a successful extend: brand new deadline. + second := time.Now().Add(60 * time.Millisecond) + _ = w.Update(second) + + // 4 events total: stateChange, warning (first), stateChange, warning (second). + events := waitForEvents(t, r, 4) + if events[2].kind != stateChange { + t.Fatalf("event[2] should be stateChange for the new deadline, got %+v", events[2]) + } + if !events[3].isWarning() { + t.Fatalf("event[3] should be a warning publish for the new deadline, got %+v", events[3]) + } +} + +func TestUpdateZeroAfterNonZeroClearsState(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(time.Hour, r) + defer w.Close() + + d := time.Now().Add(2 * time.Hour) + _ = w.Update(d) + waitForEvents(t, r, 1) + + _ = w.Update(time.Time{}) + + events := waitForEvents(t, r, 2) + if events[1].kind != stateChange { + t.Fatalf("expected stateChange on clear, got %+v", events[1]) + } + if !w.Deadline().IsZero() { + t.Fatalf("Deadline should be zero after clear") + } +} + +func TestUpdateRejectsBeforeEpoch(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(50*time.Millisecond, r) + defer w.Close() + + good := time.Now().Add(time.Hour) + if err := w.Update(good); err != nil { + t.Fatalf("seed Update: %v", err) + } + + err := w.Update(time.Unix(-100, 0)) + if !errors.Is(err, ErrDeadlineBeforeEpoch) { + t.Fatalf("want ErrDeadlineBeforeEpoch, got %v", err) + } + if !w.Deadline().IsZero() { + t.Fatalf("rejected pre-epoch update must clear deadline; got %v", w.Deadline()) + } +} + +func TestUpdateRejectsTooFarFuture(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(50*time.Millisecond, r) + defer w.Close() + + good := time.Now().Add(time.Hour) + if err := w.Update(good); err != nil { + t.Fatalf("seed Update: %v", err) + } + + err := w.Update(time.Now().Add(50 * 365 * 24 * time.Hour)) + if !errors.Is(err, ErrDeadlineTooFarFuture) { + t.Fatalf("want ErrDeadlineTooFarFuture, got %v", err) + } + if !w.Deadline().IsZero() { + t.Fatalf("rejected far-future update must clear deadline; got %v", w.Deadline()) + } +} + +func TestUpdateInPastClearsDeadline(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(50*time.Millisecond, r) + defer w.Close() + + good := time.Now().Add(time.Hour) + if err := w.Update(good); err != nil { + t.Fatalf("seed Update: %v", err) + } + // Drain the stateChange from the seed. + waitForEvents(t, r, 1) + + err := w.Update(time.Now().Add(-1 * time.Hour)) + if !errors.Is(err, ErrDeadlineInPast) { + t.Fatalf("want ErrDeadlineInPast, got %v", err) + } + if !w.Deadline().IsZero() { + t.Fatalf("in-past update must clear the deadline, got %v", w.Deadline()) + } + events := waitForEvents(t, r, 2) + if events[1].kind != stateChange { + t.Fatalf("expected stateChange on clear, got %+v", events[1]) + } +} + +func TestUpdateWithinSkewAccepted(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(50*time.Millisecond, r) + defer w.Close() + + // 5 seconds in the past is within the 30s Skew tolerance — accept it. + d := time.Now().Add(-5 * time.Second) + if err := w.Update(d); err != nil { + t.Fatalf("within-skew Update should succeed, got %v", err) + } + if !w.Deadline().Equal(d) { + t.Fatalf("expected deadline to be applied, got %v want %v", w.Deadline(), d) + } +} + +func TestCloseSilencesUpdates(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(50*time.Millisecond, r) + w.Close() + + _ = w.Update(time.Now().Add(time.Hour)) + + time.Sleep(20 * time.Millisecond) + if got := r.snapshot(); len(got) != 0 { + t.Fatalf("expected no events after Close, got %+v", got) + } +} + +// TestCloseClearsRecorderDeadline pins the profile-switch fix: a watcher +// holding a live deadline must zero the recorder on Close so the next +// engine's watcher (and the UI reading the shared server-scoped recorder) +// doesn't start out showing the previous session's stale "expires in". +func TestCloseClearsRecorderDeadline(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(time.Hour, r) + + d := time.Now().Add(2 * time.Hour) + if err := w.Update(d); err != nil { + t.Fatalf("seed Update: %v", err) + } + if got := r.deadline(); !got.Equal(d) { + t.Fatalf("recorder deadline after Update = %v, want %v", got, d) + } + + w.Close() + + if got := r.deadline(); !got.IsZero() { + t.Fatalf("recorder deadline after Close = %v, want zero", got) + } +} + +// TestCloseWithoutDeadlineLeavesRecorderUntouched guards the symmetric +// case: closing a watcher that never held a deadline must not emit a +// redundant clear (the recorder may legitimately hold a value written by +// some other path; the watcher only owns what it set). +func TestCloseWithoutDeadlineLeavesRecorderUntouched(t *testing.T) { + r := &fakeRecorder{} + w := newWatcher(time.Hour, r) + + w.Close() + + if got := r.snapshot(); len(got) != 0 { + t.Fatalf("expected no events from Close on an empty watcher, got %+v", got) + } +} + +func TestFinalWarningFiresAfterRegularWarning(t *testing.T) { + r := &fakeRecorder{} + // Warning fires at deadline-80ms, final at deadline-30ms. + w := NewWithLeads(80*time.Millisecond, 30*time.Millisecond, r) + defer w.Close() + + d := time.Now().Add(100 * time.Millisecond) + _ = w.Update(d) + + // Expect stateChange + warning + final-warning. + events := waitForEvents(t, r, 3) + + if countWhere(events, func(e event) bool { return e.kind == stateChange }) != 1 { + t.Fatalf("expected exactly 1 stateChange, got %+v", events) + } + if countWhere(events, event.isWarning) != 1 { + t.Fatalf("expected exactly 1 warning publish, got %+v", events) + } + if countWhere(events, event.isFinalWarning) != 1 { + t.Fatalf("expected exactly 1 final-warning publish, got %+v", events) + } + + // Warning must precede final (same deadline, longer lead fires first). + var wIdx, fIdx int + for i, e := range events { + switch { + case e.isWarning(): + wIdx = i + case e.isFinalWarning(): + fIdx = i + } + } + if wIdx > fIdx { + t.Fatalf("warning must publish before final-warning, got order %+v", events) + } +} + +func TestDismissSuppressesFinalWarning(t *testing.T) { + r := &fakeRecorder{} + w := NewWithLeads(80*time.Millisecond, 30*time.Millisecond, r) + defer w.Close() + + d := time.Now().Add(100 * time.Millisecond) + _ = w.Update(d) + + // Wait for the warning publish so we know we're inside the warning + // window, then dismiss before the final timer would fire. + deadline := time.Now().Add(500 * time.Millisecond) + for time.Now().Before(deadline) { + if countWhere(r.snapshot(), event.isWarning) >= 1 { + break + } + time.Sleep(2 * time.Millisecond) + } + if countWhere(r.snapshot(), event.isWarning) < 1 { + t.Fatalf("warning did not publish in time, events=%+v", r.snapshot()) + } + + w.Dismiss() + + // Now wait past when the final would have fired. + time.Sleep(120 * time.Millisecond) + + if n := countWhere(r.snapshot(), event.isFinalWarning); n != 0 { + t.Fatalf("final-warning published after Dismiss(), events=%+v", r.snapshot()) + } +} + +func TestDismissResetByNewDeadline(t *testing.T) { + r := &fakeRecorder{} + w := NewWithLeads(80*time.Millisecond, 30*time.Millisecond, r) + defer w.Close() + + first := time.Now().Add(100 * time.Millisecond) + _ = w.Update(first) + + // Dismiss against the first deadline. + w.Dismiss() + + // Replace with a fresh deadline before the first's timers complete. + time.Sleep(10 * time.Millisecond) + second := time.Now().Add(100 * time.Millisecond) + _ = w.Update(second) + + // The second cycle must publish a final-warning (the dismiss state + // did not carry over). + deadline := time.Now().Add(500 * time.Millisecond) + for time.Now().Before(deadline) { + if countWhere(r.snapshot(), event.isFinalWarning) >= 1 { + break + } + time.Sleep(5 * time.Millisecond) + } + if countWhere(r.snapshot(), event.isFinalWarning) < 1 { + t.Fatalf("final-warning did not publish on fresh deadline after Dismiss reset, events=%+v", r.snapshot()) + } +} + +func TestDismissBeforeUpdateIsNoop(t *testing.T) { + r := &fakeRecorder{} + w := NewWithLeads(80*time.Millisecond, 30*time.Millisecond, r) + defer w.Close() + + // No deadline tracked yet; Dismiss must be a no-op (no panic, no state). + w.Dismiss() + + d := time.Now().Add(100 * time.Millisecond) + _ = w.Update(d) + + // Final warning should still publish — Dismiss only acts on the current + // deadline, and there was none at the time of the call. + deadline := time.Now().Add(500 * time.Millisecond) + for time.Now().Before(deadline) { + if countWhere(r.snapshot(), event.isFinalWarning) >= 1 { + return + } + time.Sleep(5 * time.Millisecond) + } + t.Fatalf("final-warning did not publish after no-op pre-Update Dismiss, events=%+v", r.snapshot()) +} diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go index 112559132..5d7308706 100644 --- a/client/internal/conn_mgr.go +++ b/client/internal/conn_mgr.go @@ -107,6 +107,37 @@ func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) er } } +// SetLocalLazyConn applies a local lazy connection override (UI / CLI / env). +// While enabledLocally is true, UpdatedRemoteFeatureFlag (management sync) is a +// no-op, so the local setting wins until it is turned off again. +func (e *ConnMgr) SetLocalLazyConn(ctx context.Context, enabled bool) error { + e.enabledLocally = enabled + + if enabled { + if e.lazyConnMgr != nil { + return nil + } + + if e.rosenpassEnabled { + log.Warnf("rosenpass connection manager is enabled, lazy connection manager will not be started") + return nil + } + + log.Infof("lazy connection manager is enabled locally") + e.initLazyManager(ctx) + e.statusRecorder.UpdateLazyConnection(true) + return e.addPeersToLazyConnManager() + } + + if e.lazyConnMgr == nil { + return nil + } + log.Infof("lazy connection manager is disabled locally") + e.closeManager(ctx) + e.statusRecorder.UpdateLazyConnection(false) + return nil +} + // UpdateRouteHAMap updates the route HA mappings in the lazy connection manager func (e *ConnMgr) UpdateRouteHAMap(haMap route.HAMap) { if !e.isStartedWithLazyMgr() { diff --git a/client/internal/connect.go b/client/internal/connect.go index d93b62bb5..371d4526a 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -260,6 +260,15 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan log.Debugf("connecting to the Management service %s", c.config.ManagementURL.Host) mgmClient, err := mgm.NewClient(engineCtx, c.config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled) if err != nil { + // On daemon shutdown / Down() the parent context is cancelled + // and the dial fails with "context canceled". Wrapping that + // into state would leave the snapshot stuck at Connecting+err + // until the backoff loop wakes up — instead let the operation + // return cleanly so the deferred state.Set(StatusIdle) takes + // effect on the next iteration. + if c.ctx.Err() != nil { + return nil + } return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err)) } mgmNotifier := statusRecorderToMgmConnStateNotifier(c.statusRecorder) @@ -393,6 +402,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan return wrapErr(err) } + // Seed the session-expiry deadline from the LoginResponse. Subsequent + // changes flow in through SyncResponse and are applied in handleSync. + engine.ApplySessionDeadline(loginResp.GetSessionExpiresAt()) + log.Infof("Netbird engine started, the IP is: %s", peerConfig.GetAddress()) state.Set(StatusConnected) @@ -433,7 +446,11 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan } c.statusRecorder.ClientStart() - err = backoff.Retry(operation, backOff) + // Wrap the backoff with c.ctx so Down()/actCancel propagates into the + // inter-attempt sleep — otherwise a 15s MaxInterval can keep the retry + // loop alive long after the caller asked to give up, leaving the + // status stream stuck at Connecting. + err = backoff.Retry(operation, backoff.WithContext(backOff, c.ctx)) if err != nil { log.Debugf("exiting client retry loop due to unrecoverable error: %s", err) if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) { diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index a65d8bd05..99f13b0b8 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -229,9 +229,16 @@ scutil_dns.txt (macOS only): const ( clientLogFile = "client.log" + uiLogFile = "gui-client.log" errorLogFile = "netbird.err" stdoutLogFile = "netbird.out" + // Rotated-log glob prefixes (base log name without extension) passed to + // addRotatedLogFiles. The daemon's own log and the GUI log live in the same + // dir, so the prefixes must be disjoint to keep their rotated siblings apart. + clientLogPrefix = "client" + uiLogPrefix = "gui-client" + darwinErrorLogPath = "/var/log/netbird.out.log" darwinStdoutLogPath = "/var/log/netbird.err.log" ) @@ -249,6 +256,7 @@ type BundleGenerator struct { statusRecorder *peer.Status syncResponse *mgmProto.SyncResponse logPath string + uiLogPath string tempDir string statePath string cpuProfile []byte @@ -276,6 +284,7 @@ type GeneratorDependencies struct { StatusRecorder *peer.Status SyncResponse *mgmProto.SyncResponse LogPath string + UILogPath string // Absolute path to the desktop UI's gui-client.log, reported via RegisterUILog. Empty if no UI registered one. TempDir string // Directory for temporary bundle zip files. If empty, os.TempDir() is used. StatePath string // Path to the state file. If empty, the ServiceManager default path is used. CPUProfile []byte @@ -300,6 +309,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen statusRecorder: deps.StatusRecorder, syncResponse: deps.SyncResponse, logPath: deps.LogPath, + uiLogPath: deps.UILogPath, tempDir: deps.TempDir, statePath: deps.StatePath, cpuProfile: deps.CPUProfile, @@ -411,6 +421,10 @@ func (g *BundleGenerator) createArchive() error { log.Errorf("failed to add logs to debug bundle: %v", err) } + if err := g.addUILog(); err != nil { + log.Errorf("failed to add UI log to debug bundle: %v", err) + } + if err := g.addUpdateLogs(); err != nil { log.Errorf("failed to add updater logs: %v", err) } @@ -986,7 +1000,7 @@ func (g *BundleGenerator) addLogfile() error { return fmt.Errorf("add client log file to zip: %w", err) } - g.addRotatedLogFiles(logDir) + g.addRotatedLogFiles(logDir, clientLogPrefix) stdErrLogPath := filepath.Join(logDir, errorLogFile) stdoutLogPath := filepath.Join(logDir, stdoutLogFile) @@ -1006,6 +1020,25 @@ func (g *BundleGenerator) addLogfile() error { return nil } +// addUILog adds the desktop UI's gui-client.log (and its rotated siblings) to +// the bundle. The path is reported by the UI via RegisterUILog; empty when no +// UI registered one (e.g. headless / server). Missing file is non-fatal — the +// UI only writes it while the daemon is in debug, so it's often absent. +func (g *BundleGenerator) addUILog() error { + if g.uiLogPath == "" { + log.Debugf("no UI log path registered, skipping in debug bundle") + return nil + } + + if err := g.addSingleLogfile(g.uiLogPath, uiLogFile); err != nil { + return fmt.Errorf("add UI log file to zip: %w", err) + } + + g.addRotatedLogFiles(filepath.Dir(g.uiLogPath), uiLogPrefix) + + return nil +} + // addSingleLogfile adds a single log file to the archive func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error { logFile, err := os.Open(logPath) @@ -1078,14 +1111,16 @@ func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error { return nil } -// addRotatedLogFiles adds rotated log files to the bundle based on logFileCount -func (g *BundleGenerator) addRotatedLogFiles(logDir string) { +// addRotatedLogFiles adds rotated log files to the bundle based on logFileCount. +// prefix is the base log name without extension (e.g. "client", "gui-client"); +// the glob matches both files rotated by us and by logrotate on linux. +func (g *BundleGenerator) addRotatedLogFiles(logDir, prefix string) { if g.logFileCount == 0 { return } - // This regex will match both logs rotated by us and logrotate on linux - pattern := filepath.Join(logDir, "client*.log.*") + // This pattern matches both logs rotated by us and logrotate on linux + pattern := filepath.Join(logDir, prefix+"*.log.*") files, err := filepath.Glob(pattern) if err != nil { log.Warnf("failed to glob rotated logs: %v", err) diff --git a/client/internal/debug/debug_logfiles_test.go b/client/internal/debug/debug_logfiles_test.go index f6473f979..3749b3e9b 100644 --- a/client/internal/debug/debug_logfiles_test.go +++ b/client/internal/debug/debug_logfiles_test.go @@ -40,6 +40,25 @@ func TestAddRotatedLogFiles_PicksUpAllVariants(t *testing.T) { require.NotContains(t, names, "other.log", "unrelated files should not be in bundle") } +// TestAddRotatedLogFiles_GUIPrefix asserts the prefix parameter scopes the glob +// to the GUI log: gui-client.log.* rotated siblings are picked up and the +// daemon's own client.log.* are not (and vice versa, covered above). This is +// the load-bearing check for the gui-client.log bundle collection — the old +// "client*.log.*" glob would have missed gui-client rotations. +func TestAddRotatedLogFiles_GUIPrefix(t *testing.T) { + dir := t.TempDir() + + writeFile(t, filepath.Join(dir, "gui-client.log.1"), "gui rotated\n") + writeGzFile(t, filepath.Join(dir, "gui-client.log.2.gz"), "gui rotated gz\n") + writeFile(t, filepath.Join(dir, "client.log.1"), "daemon rotated\n") + + names := runAddRotatedLogFilesPrefix(t, dir, "gui-client", 10) + + require.Contains(t, names, "gui-client.log.1", "gui-client rotated file should be in bundle") + require.Contains(t, names, "gui-client.log.2.gz", "gui-client gz rotated file should be in bundle") + require.NotContains(t, names, "client.log.1", "daemon rotated file must not match the gui-client prefix") +} + // TestAddRotatedLogFiles_RespectsLogFileCount asserts that only the newest // logFileCount rotated files are bundled, ordered by mtime. func TestAddRotatedLogFiles_RespectsLogFileCount(t *testing.T) { @@ -67,6 +86,10 @@ func TestAddRotatedLogFiles_RespectsLogFileCount(t *testing.T) { // runAddRotatedLogFiles calls addRotatedLogFiles against a fresh in-memory // zip writer and returns the set of entry names that ended up in the archive. func runAddRotatedLogFiles(t *testing.T, dir string, logFileCount uint32) map[string]struct{} { + return runAddRotatedLogFilesPrefix(t, dir, "client", logFileCount) +} + +func runAddRotatedLogFilesPrefix(t *testing.T, dir, prefix string, logFileCount uint32) map[string]struct{} { t.Helper() var buf bytes.Buffer @@ -74,7 +97,7 @@ func runAddRotatedLogFiles(t *testing.T, dir string, logFileCount uint32) map[st archive: zip.NewWriter(&buf), logFileCount: logFileCount, } - g.addRotatedLogFiles(dir) + g.addRotatedLogFiles(dir, prefix) require.NoError(t, g.archive.Close()) zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) diff --git a/client/internal/engine.go b/client/internal/engine.go index 42712da92..1226e5102 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -259,6 +259,20 @@ type Engine struct { jobExecutorWG sync.WaitGroup exposeManager *expose.Manager + + sessionWatcher sessionDeadlineWatcher +} + +// sessionDeadlineWatcher is the engine-facing surface of the SSO session +// expiry watcher. The concrete implementation (sessionwatch.Watcher) is wired +// in via newSessionWatcher, which is build-tagged so the js/wasm build links a +// no-op stub instead of pulling the full sessionwatch package (and its timer +// machinery) into the binary — the wasm client never runs the engine's +// session-warning flow. +type sessionDeadlineWatcher interface { + Update(deadline time.Time) error + Dismiss() + Close() } // Peer is an instance of the Connection Peer @@ -303,6 +317,17 @@ func NewEngine( updateManager: services.UpdateManager, syncStoreDir: config.StateDir, } + // sessionWatcher keeps the SubscribeStatus consumers in sync with the + // session expiry deadline. Deadline-change ticks come for free via + // Status.SetSessionExpiresAt; the watcher exists to push a wake-up at + // T-WarningLead and T-FinalWarningLead so the UI repaints the remaining + // time / warning state even when nothing else changed, and to publish + // two SystemEvents (the warning composition lives in sessionwatch so + // the wire format stays owned by one package): + // - T-WarningLead → interactive "Extend now / Dismiss" notification + // - T-FinalWarningLead → auto-opened SessionAboutToExpire dialog, + // suppressed when the user dismissed the earlier warning + engine.sessionWatcher = newSessionWatcher(engine.statusRecorder) log.Infof("I am: %s", config.WgPrivateKey.PublicKey().String()) return engine @@ -343,6 +368,10 @@ func (e *Engine) Stop() error { e.srWatcher.Close() } + if e.sessionWatcher != nil { + e.sessionWatcher.Close() + } + if e.updateManager != nil { e.updateManager.SetDownloadOnly() } @@ -879,6 +908,8 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { return e.ctx.Err() } + e.ApplySessionDeadline(update.GetSessionExpiresAt()) + if update.NetworkMap != nil && update.NetworkMap.PeerConfig != nil { e.handleAutoUpdateVersion(update.NetworkMap.PeerConfig.AutoUpdate) } @@ -1178,7 +1209,7 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR ClientMetrics: e.clientMetrics, DaemonVersion: version.NetbirdVersion(), RefreshStatus: func() { - e.RunHealthProbes(true) + e.RunHealthProbes(e.ctx, true) }, } @@ -2081,7 +2112,20 @@ func (e *Engine) getRosenpassAddr() string { // RunHealthProbes executes health checks for Signal, Management, Relay, and WireGuard services // and updates the status recorder with the latest states. -func (e *Engine) RunHealthProbes(waitForResult bool) bool { +// +// ctx scopes the (potentially slow) STUN/TURN probing: a caller that gives up — +// e.g. a Status RPC whose client disconnected — cancels its ctx and the probe +// returns instead of running to its per-component timeout. The engine's own +// lifetime ctx still applies independently, so an engine shutdown aborts the +// probe even if the caller's ctx is context.Background(). +func (e *Engine) RunHealthProbes(ctx context.Context, waitForResult bool) bool { + // Tie the caller's ctx to the engine lifetime: either cancelling aborts + // the probe below. + ctx, cancel := context.WithCancel(ctx) + defer cancel() + stop := context.AfterFunc(e.ctx, cancel) + defer stop() + e.syncMsgMux.Lock() signalHealthy := e.signal.IsHealthy() @@ -2104,9 +2148,9 @@ func (e *Engine) RunHealthProbes(waitForResult bool) bool { if runtime.GOOS != "js" { var results []relay.ProbeResult if waitForResult { - results = e.probeStunTurn.ProbeAllWaitResult(e.ctx, stuns, turns) + results = e.probeStunTurn.ProbeAllWaitResult(ctx, stuns, turns) } else { - results = e.probeStunTurn.ProbeAll(e.ctx, stuns, turns) + results = e.probeStunTurn.ProbeAll(ctx, stuns, turns) } e.statusRecorder.UpdateRelayStates(results) diff --git a/client/internal/engine_authsession.go b/client/internal/engine_authsession.go new file mode 100644 index 000000000..725c0903f --- /dev/null +++ b/client/internal/engine_authsession.go @@ -0,0 +1,108 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/netbirdio/netbird/client/internal/auth/sessionwatch" + cProto "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/client/system" +) + +// ApplySessionDeadline propagates the absolute SSO session deadline carried on +// LoginResponse / SyncResponse to both the watcher (for the edge-triggered +// warning) and the status recorder (for the SubscribeStatus / Status RPC +// snapshot the UI consumes). +// +// The wire field is 3-state: +// - nil → snapshot carries no info; keep the +// previously-anchored deadline (no-op) +// - explicit zero (s=0, n=0) → peer is not SSO-registered or expiry is +// disabled; clear both sinks +// - valid timestamp → new deadline; arm watcher, expose on +// status recorder +// +// Deadline sanity-checks live in sessionwatch.Watcher.Update. Any rejected +// value is treated as a clear on both sinks: the alternative — leaving the +// previously-known deadline in place — risks the UI confidently displaying +// a stale "expires in X" while the server has actually invalidated it. +func (e *Engine) ApplySessionDeadline(ts *timestamppb.Timestamp) { + if ts == nil { + return + } + var deadline time.Time + // Explicit zero (seconds=0 AND nanos=0) is the sentinel for "disabled". + // Everything else flows through Watcher.Update, whose sanity-checks + // reject out-of-range / pre-epoch / far-future / too-stale values and + // clear on rejection. + if ts.GetSeconds() != 0 || ts.GetNanos() != 0 { + deadline = ts.AsTime().UTC() + } + if e.sessionWatcher == nil { + return + } + // Watcher.Update owns the propagation to the status recorder (the + // SubscribeStatus / Status snapshot the UI reads): a set writes the + // deadline, a clear or a sanity-check rejection writes the zero value. + // Keeping a single writer is what stops the recorder from drifting out + // 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()}, + ) + } +} + +// DismissSessionWarning records the user's "Dismiss" click on the +// T-WarningLead interactive notification and suppresses the upcoming +// T-FinalWarningLead fallback for the current deadline. No-op when the +// watcher is not running or holds no deadline. +func (e *Engine) DismissSessionWarning() { + if e.sessionWatcher == nil { + return + } + e.sessionWatcher.Dismiss() +} + +// ExtendAuthSession asks the management server to refresh the SSO session +// expiry deadline using the supplied JWT, then mirrors the new deadline into +// the daemon's state. The tunnel is untouched; no resync, no reconnect. +// +// Returns the new absolute UTC deadline (or zero time when the server +// reports the peer is not eligible for extension). +func (e *Engine) ExtendAuthSession(ctx context.Context, jwtToken string) (time.Time, error) { + if jwtToken == "" { + return time.Time{}, errors.New("jwt token is required") + } + if e.mgmClient == nil { + return time.Time{}, errors.New("management client is not initialised") + } + + info, err := system.GetInfoWithChecks(ctx, e.checks) + if err != nil { + log.Warnf("failed to collect system info for session extend: %v", err) + info = system.GetInfo(ctx) + } + + resp, err := e.mgmClient.ExtendAuthSession(info, jwtToken) + if err != nil { + return time.Time{}, fmt.Errorf("extend auth session on management: %w", err) + } + + e.ApplySessionDeadline(resp.GetSessionExpiresAt()) + + if resp.GetSessionExpiresAt().IsValid() { + return resp.GetSessionExpiresAt().AsTime().UTC(), nil + } + return time.Time{}, nil +} diff --git a/client/internal/engine_lazyconn.go b/client/internal/engine_lazyconn.go new file mode 100644 index 000000000..4910ccf72 --- /dev/null +++ b/client/internal/engine_lazyconn.go @@ -0,0 +1,19 @@ +package internal + +import ( + "errors" +) + +// SetLazyConnEnabled applies a local lazy connection override to the running +// engine. It pins the setting like an env/CLI flag, so a later management sync +// cannot override it. syncMsgMux guards ConnMgr, which is not thread-safe. +func (e *Engine) SetLazyConnEnabled(enabled bool) error { + e.syncMsgMux.Lock() + defer e.syncMsgMux.Unlock() + + if e.connMgr == nil { + return errors.New("connection manager is not initialised") + } + + return e.connMgr.SetLocalLazyConn(e.ctx, enabled) +} diff --git a/client/internal/engine_session_deadline_test.go b/client/internal/engine_session_deadline_test.go new file mode 100644 index 000000000..6127e5bb0 --- /dev/null +++ b/client/internal/engine_session_deadline_test.go @@ -0,0 +1,78 @@ +package internal + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/netbirdio/netbird/client/internal/auth/sessionwatch" + "github.com/netbirdio/netbird/client/internal/peer" +) + +// TestApplySessionDeadline_ThreeState pins down the 3-state semantics of the +// wire field carried on LoginResponse / SyncResponse: +// +// - nil pointer → no info; previously-anchored deadline survives +// - explicit zero value → "expiry disabled" sentinel; both sinks cleared +// - valid future timestamp → new deadline propagated to both sinks +func TestApplySessionDeadline_ThreeState(t *testing.T) { + newEngine := func() *Engine { + recorder := peer.NewRecorder("") + return &Engine{ + statusRecorder: recorder, + sessionWatcher: sessionwatch.New(recorder), + } + } + + t.Run("valid timestamp sets deadline on both sinks", func(t *testing.T) { + e := newEngine() + deadline := time.Now().Add(time.Hour).UTC().Truncate(time.Second) + + e.ApplySessionDeadline(timestamppb.New(deadline)) + + require.True(t, e.statusRecorder.GetSessionExpiresAt().Equal(deadline), + "status recorder should hold the new deadline") + }) + + t.Run("nil is a no-op and preserves previous deadline", func(t *testing.T) { + e := newEngine() + seeded := time.Now().Add(time.Hour).UTC().Truncate(time.Second) + e.ApplySessionDeadline(timestamppb.New(seeded)) + require.True(t, e.statusRecorder.GetSessionExpiresAt().Equal(seeded)) + + e.ApplySessionDeadline(nil) + + require.True(t, e.statusRecorder.GetSessionExpiresAt().Equal(seeded), + "nil snapshot must not disturb the existing deadline") + }) + + t.Run("explicit zero clears a previously-anchored deadline", func(t *testing.T) { + e := newEngine() + seeded := time.Now().Add(time.Hour).UTC().Truncate(time.Second) + e.ApplySessionDeadline(timestamppb.New(seeded)) + require.True(t, e.statusRecorder.GetSessionExpiresAt().Equal(seeded)) + + // Explicit zero Timestamp{} (seconds=0, nanos=0) is the + // "expiry disabled / not SSO" sentinel. + e.ApplySessionDeadline(×tamppb.Timestamp{}) + + require.True(t, e.statusRecorder.GetSessionExpiresAt().IsZero(), + "explicit zero sentinel must clear the deadline") + }) + + t.Run("invalid timestamp clears the deadline", func(t *testing.T) { + e := newEngine() + seeded := time.Now().Add(time.Hour).UTC().Truncate(time.Second) + e.ApplySessionDeadline(timestamppb.New(seeded)) + require.True(t, e.statusRecorder.GetSessionExpiresAt().Equal(seeded)) + + // Out-of-range nanos → IsValid()==false; same-meaning as the + // disabled sentinel for downstream sinks. + e.ApplySessionDeadline(×tamppb.Timestamp{Seconds: 1, Nanos: -1}) + + require.True(t, e.statusRecorder.GetSessionExpiresAt().IsZero(), + "invalid timestamp must clear the deadline") + }) +} diff --git a/client/internal/engine_sessionwatch.go b/client/internal/engine_sessionwatch.go new file mode 100644 index 000000000..a46d73f87 --- /dev/null +++ b/client/internal/engine_sessionwatch.go @@ -0,0 +1,16 @@ +//go:build !js + +package internal + +import ( + "github.com/netbirdio/netbird/client/internal/auth/sessionwatch" + "github.com/netbirdio/netbird/client/internal/peer" +) + +// newSessionWatcher returns the real SSO session expiry watcher for every +// non-wasm build. The js/wasm build gets a no-op stub from +// engine_sessionwatch_js.go so the sessionwatch package (and its timer +// machinery) never links into the wasm binary. +func newSessionWatcher(recorder *peer.Status) sessionDeadlineWatcher { + return sessionwatch.New(recorder) +} diff --git a/client/internal/engine_sessionwatch_js.go b/client/internal/engine_sessionwatch_js.go new file mode 100644 index 000000000..50e148ab9 --- /dev/null +++ b/client/internal/engine_sessionwatch_js.go @@ -0,0 +1,44 @@ +//go:build js + +package internal + +import ( + "time" + + "github.com/netbirdio/netbird/client/internal/peer" +) + +// noopSessionWatcher is the js/wasm stand-in for sessionwatch.Watcher. The +// wasm client never runs the engine's session-warning flow (the interactive +// T-WarningLead notification and the T-FinalWarningLead fallback dialog live +// in the desktop UI), so linking the full sessionwatch package (timers, event +// composition) would only bloat the binary. +// +// It still mirrors the deadline into the status recorder so the SubscribeStatus +// / Status snapshot the UI consumes stays correct — only the timer-driven +// warnings are dropped. +type noopSessionWatcher struct { + recorder *peer.Status +} + +func newSessionWatcher(recorder *peer.Status) sessionDeadlineWatcher { + return noopSessionWatcher{recorder: recorder} +} + +// Update mirrors the real watcher's recorder propagation without the timers or +// sanity-check sentinels: a valid deadline is exposed on the status snapshot, +// the zero time clears it. +func (w noopSessionWatcher) Update(deadline time.Time) error { + if w.recorder != nil { + w.recorder.SetSessionExpiresAt(deadline) + } + return nil +} + +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/internal/peer/status.go b/client/internal/peer/status.go index 3e5c56dd2..0e9359450 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -7,6 +7,7 @@ import ( "net/netip" "slices" "sync" + "sync/atomic" "time" "github.com/google/uuid" @@ -191,22 +192,29 @@ func (s *StatusChangeSubscription) Events() chan map[string]RouterState { // every private-service request) don't contend against each other. // Pure read methods take RLock; anything that mutates state takes Lock. type Status struct { - mux sync.RWMutex - peers map[string]State - ipToKey map[string]string - changeNotify map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription - signalState bool - signalError error - managementState bool - managementError error - relayStates []relay.ProbeResult - localPeer LocalPeerState - offlinePeers []State - mgmAddress string - signalAddress string - notifier *notifier - rosenpassEnabled bool - rosenpassPermissive bool + mux sync.RWMutex + peers map[string]State + ipToKey map[string]string + changeNotify map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription + signalState bool + signalError error + managementState bool + managementError error + relayStates []relay.ProbeResult + localPeer LocalPeerState + offlinePeers []State + mgmAddress string + signalAddress string + notifier *notifier + rosenpassEnabled bool + rosenpassPermissive bool + // sessionExpiresAt is the absolute UTC instant at which the peer's SSO + // session expires. Zero when the peer is not SSO-tracked or login + // expiration is disabled. Populated from management LoginResponse / + // SyncResponse and exposed via the daemon's Status / SubscribeStatus RPC + // so the UI can show remaining time without itself talking to mgm. + sessionExpiresAt time.Time + nsGroupStates []NSGroupState resolvedDomainsStates map[domain.Domain]ResolvedDomainInfo lazyConnectionEnabled bool @@ -222,6 +230,21 @@ type Status struct { eventStreams map[string]chan *proto.SystemEvent eventQueue *EventQueue + // stateChangeStreams fan-out connection-state changes (connected / + // disconnected / connecting / address change / peers list change) to + // every active SubscribeStatus gRPC stream. Each subscriber gets a + // buffered chan; the notifier non-blockingly pings them so a slow + // consumer can never stall the daemon. + stateChangeMux sync.Mutex + stateChangeStreams map[string]chan struct{} + + // networksRevision bumps whenever the routed-networks set or their + // selected state changes (driven by the route manager). Surfaced in the + // status snapshot so the UI can fingerprint on it and re-fetch + // ListNetworks only on a real change. Atomic so the snapshot builder can + // read it without taking mux. + networksRevision atomic.Uint64 + ingressGwMgr *ingressgw.Manager routeIDLookup routeIDLookup @@ -236,6 +259,7 @@ func NewRecorder(mgmAddress string) *Status { changeNotify: make(map[string]map[string]*StatusChangeSubscription), eventStreams: make(map[string]chan *proto.SystemEvent), eventQueue: NewEventQueue(eventQueueSize), + stateChangeStreams: make(map[string]chan struct{}), offlinePeers: make([]State, 0), notifier: newNotifier(), mgmAddress: mgmAddress, @@ -400,6 +424,7 @@ func (d *Status) UpdatePeerState(receivedState State) error { if notifyRouter { d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot) } + d.notifyStateChange() return nil } @@ -425,6 +450,7 @@ func (d *Status) AddPeerStateRoute(peer string, route string, resourceId route.R // todo: consider to make sense of this notification or not d.notifier.peerListChanged(numPeers) + d.notifyStateChange() return nil } @@ -450,6 +476,7 @@ func (d *Status) RemovePeerStateRoute(peer string, route string) error { // todo: consider to make sense of this notification or not d.notifier.peerListChanged(numPeers) + d.notifyStateChange() return nil } @@ -499,6 +526,7 @@ func (d *Status) UpdatePeerICEState(receivedState State) error { if notifyRouter { d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot) } + d.notifyStateChange() return nil } @@ -535,6 +563,7 @@ func (d *Status) UpdatePeerRelayedState(receivedState State) error { if notifyRouter { d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot) } + d.notifyStateChange() return nil } @@ -570,6 +599,7 @@ func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error if notifyRouter { d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot) } + d.notifyStateChange() return nil } @@ -608,6 +638,7 @@ func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error { if notifyRouter { d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot) } + d.notifyStateChange() return nil } @@ -701,6 +732,7 @@ func (d *Status) FinishPeerListModifications() { for _, rd := range dispatches { d.dispatchRouterPeers(rd.peerID, rd.snapshot) } + d.notifyStateChange() } func (d *Status) SubscribeToPeerStateChanges(ctx context.Context, peerID string) *StatusChangeSubscription { @@ -759,6 +791,41 @@ func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) { d.mux.Unlock() d.notifier.localAddressChanged(fqdn, ip) + d.notifyStateChange() +} + +// SetSessionExpiresAt records the absolute UTC instant at which the peer's +// SSO session is set to expire. Pass the zero value to clear (e.g. when the +// management server stops publishing a deadline because login expiration was +// disabled or the peer is not SSO-tracked). Same-value updates are no-ops; +// real changes fan out via notifyStateChange so SubscribeStatus consumers +// pick up the new deadline on their next read. +func (d *Status) SetSessionExpiresAt(deadline time.Time) { + d.mux.Lock() + if d.sessionExpiresAt.Equal(deadline) { + d.mux.Unlock() + return + } + d.sessionExpiresAt = deadline + d.mux.Unlock() + d.notifyStateChange() +} + +// GetSessionExpiresAt returns the most recently recorded SSO session deadline, +// or the zero value when no deadline is tracked. A deadline that has already +// slipped into the past reports as "none": once the session has expired it is +// no longer a meaningful countdown, and the sessionwatch.Watcher does not +// arm a timer at the deadline itself to clear it (only the two pre-expiry +// warnings). Without this guard the UI would keep painting a stale +// "expires in …" against a moment that has passed until the next login, +// extend, or teardown rewrote the value. +func (d *Status) GetSessionExpiresAt() time.Time { + d.mux.Lock() + defer d.mux.Unlock() + if !d.sessionExpiresAt.IsZero() && d.sessionExpiresAt.Before(time.Now()) { + return time.Time{} + } + return d.sessionExpiresAt } // AddLocalPeerStateRoute adds a route to the local peer state @@ -827,11 +894,19 @@ func (d *Status) CleanLocalPeerState() { d.mux.Unlock() d.notifier.localAddressChanged(fqdn, ip) + d.notifyStateChange() } // MarkManagementDisconnected sets ManagementState to disconnected func (d *Status) MarkManagementDisconnected(err error) { d.mux.Lock() + // Health checks re-mark the same state on every probe; skip the fan-out + // when nothing actually changed so we don't flood SubscribeStatus + // consumers with identical snapshots. + if !d.managementState && errors.Is(d.managementError, err) { + d.mux.Unlock() + return + } d.managementState = false d.managementError = err mgm := d.managementState @@ -839,11 +914,16 @@ func (d *Status) MarkManagementDisconnected(err error) { d.mux.Unlock() d.notifier.updateServerStates(mgm, sig) + d.notifyStateChange() } // MarkManagementConnected sets ManagementState to connected func (d *Status) MarkManagementConnected() { d.mux.Lock() + if d.managementState && d.managementError == nil { + d.mux.Unlock() + return + } d.managementState = true d.managementError = nil mgm := d.managementState @@ -851,6 +931,7 @@ func (d *Status) MarkManagementConnected() { d.mux.Unlock() d.notifier.updateServerStates(mgm, sig) + d.notifyStateChange() } // UpdateSignalAddress update the address of the signal server @@ -884,6 +965,10 @@ func (d *Status) UpdateLazyConnection(enabled bool) { // MarkSignalDisconnected sets SignalState to disconnected func (d *Status) MarkSignalDisconnected(err error) { d.mux.Lock() + if !d.signalState && errors.Is(d.signalError, err) { + d.mux.Unlock() + return + } d.signalState = false d.signalError = err mgm := d.managementState @@ -891,11 +976,16 @@ func (d *Status) MarkSignalDisconnected(err error) { d.mux.Unlock() d.notifier.updateServerStates(mgm, sig) + d.notifyStateChange() } // MarkSignalConnected sets SignalState to connected func (d *Status) MarkSignalConnected() { d.mux.Lock() + if d.signalState && d.signalError == nil { + d.mux.Unlock() + return + } d.signalState = true d.signalError = nil mgm := d.managementState @@ -903,6 +993,7 @@ func (d *Status) MarkSignalConnected() { d.mux.Unlock() d.notifier.updateServerStates(mgm, sig) + d.notifyStateChange() } func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) { @@ -1107,16 +1198,19 @@ func (d *Status) GetFullStatus() FullStatus { // ClientStart will notify all listeners about the new service state func (d *Status) ClientStart() { d.notifier.clientStart() + d.notifyStateChange() } // ClientStop will notify all listeners about the new service state func (d *Status) ClientStop() { d.notifier.clientStop() + d.notifyStateChange() } // ClientTeardown will notify all listeners about the service is under teardown func (d *Status) ClientTeardown() { d.notifier.clientTearDown() + d.notifyStateChange() } // SetConnectionListener set a listener to the notifier @@ -1258,6 +1352,79 @@ func (d *Status) GetEventHistory() []*proto.SystemEvent { return d.eventQueue.GetAll() } +// SubscribeToStateChanges hands back a channel that receives a tick on +// every connection-state change (connected / disconnected / connecting / +// address change / peers-list change). The channel is buffered to one +// pending tick so a coalesced burst still wakes the consumer exactly +// once. Pass the returned id to UnsubscribeFromStateChanges to detach. +func (d *Status) SubscribeToStateChanges() (string, <-chan struct{}) { + d.stateChangeMux.Lock() + defer d.stateChangeMux.Unlock() + + id := uuid.New().String() + ch := make(chan struct{}, 1) + d.stateChangeStreams[id] = ch + return id, ch +} + +// UnsubscribeFromStateChanges releases a SubscribeToStateChanges channel +// and closes it so any consumer goroutine selecting on the channel +// unblocks cleanly. +func (d *Status) UnsubscribeFromStateChanges(id string) { + d.stateChangeMux.Lock() + defer d.stateChangeMux.Unlock() + + if ch, ok := d.stateChangeStreams[id]; ok { + close(ch) + delete(d.stateChangeStreams, id) + } +} + +// notifyStateChange wakes every SubscribeToStateChanges subscriber. Drops +// the tick if a subscriber's buffer is full — by definition the consumer +// is already going to fetch the latest snapshot, so multiple pending ticks +// would be redundant. +func (d *Status) notifyStateChange() { + d.stateChangeMux.Lock() + defer d.stateChangeMux.Unlock() + + for _, ch := range d.stateChangeStreams { + select { + case ch <- struct{}{}: + default: + } + } +} + +// NotifyStateChange is the public wake-the-subscribers entry point used by +// callers that mutate state outside the peer recorder — most importantly +// the connect-state machine, which writes StatusNeedsLogin into the +// shared contextState (client/internal/state.go) without touching any +// recorder field. Without this push the SubscribeStatus stream stays on +// the previous snapshot until an unrelated peer/management/signal +// change happens to fire notifyStateChange, leaving the UI's status +// out of sync with the daemon. +func (d *Status) NotifyStateChange() { + d.notifyStateChange() +} + +// BumpNetworksRevision increments the routed-networks revision and wakes every +// SubscribeStatus subscriber. The route manager calls it when a network map +// changes the available routes or when a selection is applied — the peer +// status itself only records actively-routed (chosen) networks, so without +// this bump a candidate route appearing/disappearing would never reach the UI. +func (d *Status) BumpNetworksRevision() { + d.networksRevision.Add(1) + d.notifyStateChange() +} + +// GetNetworksRevision returns the current routed-networks revision, surfaced in +// the status snapshot so the UI can detect route/selection changes (see +// BumpNetworksRevision). +func (d *Status) GetNetworksRevision() uint64 { + return d.networksRevision.Load() +} + func (d *Status) SetWgIface(wgInterface WGIfaceStatus) { d.mux.Lock() defer d.mux.Unlock() diff --git a/client/internal/peer/status_test.go b/client/internal/peer/status_test.go index 17ed47cd3..29404d413 100644 --- a/client/internal/peer/status_test.go +++ b/client/internal/peer/status_test.go @@ -314,3 +314,39 @@ func TestGetFullStatus(t *testing.T) { assert.Equal(t, signalState, fullStatus.SignalState, "signal status should be equal") assert.ElementsMatch(t, []State{peerState1, peerState2}, fullStatus.Peers, "peers states should match") } + +// notified reports whether a state-change tick is pending on ch, draining it. +func notified(ch <-chan struct{}) bool { + select { + case <-ch: + return true + default: + return false + } +} + +func TestMarkServerStateDoesNotNotifyWhenUnchanged(t *testing.T) { + status := NewRecorder("https://mgm") + _, ch := status.SubscribeToStateChanges() + + // First transition is a real change and must notify. + status.MarkManagementConnected() + require.True(t, notified(ch), "first connect should notify") + + // Re-marking the same state must not notify again. + status.MarkManagementConnected() + assert.False(t, notified(ch), "redundant connect should not notify") + + // Same for signal. + status.MarkSignalConnected() + require.True(t, notified(ch), "first signal connect should notify") + status.MarkSignalConnected() + assert.False(t, notified(ch), "redundant signal connect should not notify") + + // A genuine change (disconnect with an error) notifies again. + err := errors.New("boom") + status.MarkManagementDisconnected(err) + require.True(t, notified(ch), "disconnect should notify") + status.MarkManagementDisconnected(err) + assert.False(t, notified(ch), "redundant disconnect should not notify") +} diff --git a/client/internal/profilemanager/state.go b/client/internal/profilemanager/state.go index 1bf3318af..9e9577796 100644 --- a/client/internal/profilemanager/state.go +++ b/client/internal/profilemanager/state.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "path/filepath" "github.com/netbirdio/netbird/util" @@ -71,3 +72,22 @@ func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error { return nil } + +// RemoveProfileState deletes the per-profile state file (which holds the +// account email used for the SSO login hint and the UI display). Called after +// a successful logout so a logged-out profile no longer shows a stale account +// email. The state file only stores the email, so deleting it is equivalent to +// clearing it; the next SSO login recreates it. A missing file is not an error. +func (pm *ProfileManager) RemoveProfileState(profileName string) error { + configDir, err := getConfigDir() + if err != nil { + return fmt.Errorf("get config directory: %w", err) + } + + stateFile := filepath.Join(configDir, profileName+".state.json") + if err := os.Remove(stateFile); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("remove profile state: %w", err) + } + + return nil +} diff --git a/client/internal/routemanager/exit_node_selection_test.go b/client/internal/routemanager/exit_node_selection_test.go new file mode 100644 index 000000000..28dd0a640 --- /dev/null +++ b/client/internal/routemanager/exit_node_selection_test.go @@ -0,0 +1,191 @@ +package routemanager + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/internal/routeselector" + "github.com/netbirdio/netbird/route" +) + +func newExitNodeTestManager() *DefaultManager { + return &DefaultManager{routeSelector: routeselector.NewRouteSelector()} +} + +func exitRoute(netID, peer string, skipAutoApply bool) *route.Route { + return &route.Route{ + NetID: route.NetID(netID), + Network: netip.MustParsePrefix("0.0.0.0/0"), + Peer: peer, + SkipAutoApply: skipAutoApply, + } +} + +func TestPickPreferredExitNode(t *testing.T) { + tests := []struct { + name string + info exitNodeInfo + want route.NetID + }{ + { + name: "persisted user selection wins over management", + info: exitNodeInfo{ + allIDs: []route.NetID{"a", "b", "c"}, + userSelected: []route.NetID{"b"}, + selectedByManagement: []route.NetID{"a"}, + }, + want: "b", + }, + { + name: "multiple user-selected self-heal to deterministic min", + info: exitNodeInfo{ + allIDs: []route.NetID{"a", "b", "c"}, + userSelected: []route.NetID{"c", "a"}, + }, + want: "a", + }, + { + name: "explicit opt-out keeps none", + info: exitNodeInfo{ + allIDs: []route.NetID{"a", "b"}, + userDeselected: []route.NetID{"a", "b"}, + }, + want: "", + }, + { + name: "fresh defaults to management auto-apply pick", + info: exitNodeInfo{ + allIDs: []route.NetID{"a", "b", "c"}, + selectedByManagement: []route.NetID{"b"}, + }, + want: "b", + }, + { + name: "no user pick and no management auto-apply selects none", + info: exitNodeInfo{ + allIDs: []route.NetID{"c", "a", "b"}, + }, + want: "", + }, + { + name: "user-deselect does not block a management auto-apply sibling", + info: exitNodeInfo{ + allIDs: []route.NetID{"a", "b"}, + userDeselected: []route.NetID{"a"}, + selectedByManagement: []route.NetID{"b"}, + }, + want: "b", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, pickPreferredExitNode(tt.info), "preferred exit node") + }) + } +} + +func TestEnforceSingleExitNode(t *testing.T) { + m := newExitNodeTestManager() + all := []route.NetID{"a", "b", "c"} + + m.enforceSingleExitNode("b", all) + assert.False(t, m.routeSelector.IsSelected("a"), "a should be deselected") + assert.True(t, m.routeSelector.IsSelected("b"), "b should be the only selected exit node") + assert.False(t, m.routeSelector.IsSelected("c"), "c should be deselected") + + // Switching the preferred node moves the single selection. + m.enforceSingleExitNode("c", all) + assert.False(t, m.routeSelector.IsSelected("a"), "a stays deselected") + assert.False(t, m.routeSelector.IsSelected("b"), "b should now be deselected") + assert.True(t, m.routeSelector.IsSelected("c"), "c should now be selected") + + // Empty preferred turns every exit node off. + m.enforceSingleExitNode("", all) + for _, id := range all { + assert.False(t, m.routeSelector.IsSelected(id), "no exit node should be selected") + } +} + +func TestEnforceSingleExitNode_RespectsDeselectAll(t *testing.T) { + m := newExitNodeTestManager() + m.routeSelector.DeselectAllRoutes() + + m.enforceSingleExitNode("b", []route.NetID{"a", "b"}) + + assert.True(t, m.routeSelector.IsDeselectAll(), "global deselect-all must stay in effect") + assert.False(t, m.routeSelector.IsSelected("b"), "no exit node should be forced on while deselect-all is set") +} + +func TestUpdateRouteSelectorFromManagement_FreshSelectsOne(t *testing.T) { + m := newExitNodeTestManager() + routes := route.HAMap{ + "exitA|0.0.0.0/0": {exitRoute("exitA", "p1", false)}, + "exitB|0.0.0.0/0": {exitRoute("exitB", "p2", false)}, + "lan|192.168.1.0/24": {{NetID: "lan", Network: netip.MustParsePrefix("192.168.1.0/24"), Peer: "p3"}}, + "exitC|0.0.0.0/0": {exitRoute("exitC", "p4", false)}, + } + + m.updateRouteSelectorFromManagement(routes) + + // Exactly one exit node (the deterministic first) is selected. + assert.True(t, m.routeSelector.IsSelected("exitA"), "exitA is the deterministic default") + assert.False(t, m.routeSelector.IsSelected("exitB"), "exitB must not also be selected") + assert.False(t, m.routeSelector.IsSelected("exitC"), "exitC must not also be selected") + // Non-exit routes are left at their default-on state. + assert.True(t, m.routeSelector.IsSelected("lan"), "non-exit route selection is untouched") +} + +func TestUpdateRouteSelectorFromManagement_HonorsPersistedPick(t *testing.T) { + m := newExitNodeTestManager() + routes := route.HAMap{ + "exitA|0.0.0.0/0": {exitRoute("exitA", "p1", false)}, + "exitB|0.0.0.0/0": {exitRoute("exitB", "p2", false)}, + } + all := []route.NetID{"exitA", "exitB"} + + // Simulate the state the runtime select path leaves behind: exactly one + // exit node explicitly selected, its sibling deselected. + require.NoError(t, m.routeSelector.SelectRoutes([]route.NetID{"exitB"}, true, all)) + require.NoError(t, m.routeSelector.DeselectRoutes([]route.NetID{"exitA"}, all)) + + m.updateRouteSelectorFromManagement(routes) + + assert.True(t, m.routeSelector.IsSelected("exitB"), "persisted pick must stay selected") + assert.False(t, m.routeSelector.IsSelected("exitA"), "the other exit node stays deselected") +} + +func TestUpdateRouteSelectorFromManagement_OptOutKeepsNone(t *testing.T) { + m := newExitNodeTestManager() + routes := route.HAMap{ + "exitA|0.0.0.0/0": {exitRoute("exitA", "p1", false)}, + "exitB|0.0.0.0/0": {exitRoute("exitB", "p2", false)}, + } + all := []route.NetID{"exitA", "exitB"} + + // User deselected exit nodes and selected none. + require.NoError(t, m.routeSelector.DeselectRoutes(all, all)) + + m.updateRouteSelectorFromManagement(routes) + + assert.False(t, m.routeSelector.IsSelected("exitA"), "opt-out keeps exitA off") + assert.False(t, m.routeSelector.IsSelected("exitB"), "opt-out keeps exitB off") +} + +func TestUpdateRouteSelectorFromManagement_NoAutoApplySelectsNone(t *testing.T) { + m := newExitNodeTestManager() + // SkipAutoApply=true: management offers the exit nodes but doesn't request + // auto-activation, so none should be selected until the user picks one. + routes := route.HAMap{ + "exitA|0.0.0.0/0": {exitRoute("exitA", "p1", true)}, + "exitB|0.0.0.0/0": {exitRoute("exitB", "p2", true)}, + } + + m.updateRouteSelectorFromManagement(routes) + + assert.False(t, m.routeSelector.IsSelected("exitA"), "no auto-apply keeps exitA off") + assert.False(t, m.routeSelector.IsSelected("exitB"), "no auto-apply keeps exitB off") +} diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 22458d575..62933dbf1 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -442,6 +442,11 @@ func (m *DefaultManager) UpdateRoutes( m.updateClientNetworks(updateSerial, filteredClientRoutes) m.notifier.OnNewRoutes(filteredClientRoutes) + // A new network map can add or drop route/exit-node candidates without + // touching any peer's chosen-route state, so the peer status alone + // wouldn't notify SubscribeStatus subscribers. Bump the revision so the + // UI re-fetches ListNetworks. + m.statusRecorder.BumpNetworksRevision() } m.clientRoutes = clientRoutes @@ -582,6 +587,10 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) { if err := m.stateManager.UpdateState((*SelectorState)(m.routeSelector)); err != nil { log.Errorf("failed to update state: %v", err) } + + // A selection change flips Network.selected without altering the candidate + // set, so bump the revision to push the new state to the UI. + m.statusRecorder.BumpNetworksRevision() } // stopObsoleteClients stops the client network watcher for the networks that are not in the new list @@ -701,7 +710,13 @@ func resolveURLsToIPs(urls []string) []net.IP { return ips } -// updateRouteSelectorFromManagement updates the route selector based on the isSelected status from the management server +// updateRouteSelectorFromManagement reconciles exit-node selection on every +// network map: it keeps at most one exit node selected — the user's persisted +// pick, else whatever management marks for auto-apply (SkipAutoApply=false), +// else none. We never auto-activate an exit node the map doesn't request; it +// stays off until the user picks it. Exit nodes are mutually exclusive, but the +// RouteSelector stores routes with default-on semantics, so without this every +// available exit node would report selected at once. func (m *DefaultManager) updateRouteSelectorFromManagement(clientRoutes route.HAMap) { m.mirrorV6ExitPairSelections(clientRoutes) @@ -712,13 +727,14 @@ func (m *DefaultManager) updateRouteSelectorFromManagement(clientRoutes route.HA return } - exitNodeInfo := m.collectExitNodeInfo(clientRoutes) - if len(exitNodeInfo.allIDs) == 0 { + info := m.collectExitNodeInfo(clientRoutes) + if len(info.allIDs) == 0 { return } - m.updateExitNodeSelections(exitNodeInfo) - m.logExitNodeUpdate(exitNodeInfo) + preferred := pickPreferredExitNode(info) + m.enforceSingleExitNode(preferred, info.allIDs) + m.logExitNodeUpdate(info, preferred) } // mirrorV6ExitPairSelections keeps every synthesized "-v6" exit route's selection @@ -746,6 +762,10 @@ type exitNodeInfo struct { userDeselected []route.NetID } +// collectExitNodeInfo categorises the available exit nodes by their persisted +// selection state. It keys on the base (v4) NetID and skips the synthesized +// "-v6" partner, which inherits its base's selection through the RouteSelector +// — counting it separately would double-count the pair. func (m *DefaultManager) collectExitNodeInfo(clientRoutes route.HAMap) exitNodeInfo { var info exitNodeInfo @@ -755,6 +775,9 @@ func (m *DefaultManager) collectExitNodeInfo(clientRoutes route.HAMap) exitNodeI } netID := haID.NetID() + if strings.HasSuffix(string(netID), route.V6ExitSuffix) { + continue + } info.allIDs = append(info.allIDs, netID) if m.routeSelector.HasUserSelectionForRoute(netID) { @@ -791,45 +814,69 @@ func (m *DefaultManager) checkManagementSelection(routes []*route.Route, netID r } } -func (m *DefaultManager) updateExitNodeSelections(info exitNodeInfo) { - routesToDeselect := m.getRoutesToDeselect(info.allIDs) - m.deselectExitNodes(routesToDeselect) - m.selectExitNodesByManagement(info.selectedByManagement, info.allIDs) +// pickPreferredExitNode chooses the single exit node to keep selected. In order: +// - a persisted user selection wins (deterministic if several survive from +// legacy state, so the set self-heals down to one); +// - otherwise activate only what management marks for auto-apply +// (SkipAutoApply=false); the lexicographically first if it marks several. +// +// Returns "" when neither holds — we never force an arbitrary exit node on. A +// route the map doesn't auto-apply stays off until the user selects it. +// info.userDeselected is informational only: an explicit deselect simply keeps +// that route out of both lists above, so it can't be picked. +func pickPreferredExitNode(info exitNodeInfo) route.NetID { + if len(info.userSelected) > 0 { + return minNetID(info.userSelected) + } + if len(info.selectedByManagement) > 0 { + return minNetID(info.selectedByManagement) + } + return "" } -func (m *DefaultManager) getRoutesToDeselect(allIDs []route.NetID) []route.NetID { - var routesToDeselect []route.NetID - for _, netID := range allIDs { - if !m.routeSelector.HasUserSelectionForRoute(netID) { - routesToDeselect = append(routesToDeselect, netID) +// enforceSingleExitNode makes preferred the only selected exit node: every other +// available exit node is deselected and preferred (if any) is selected, without +// disturbing non-exit route selections. A global deselect-all is left untouched +// so the user's "all off" stays in effect. +func (m *DefaultManager) enforceSingleExitNode(preferred route.NetID, allIDs []route.NetID) { + if m.routeSelector.IsDeselectAll() { + return + } + + others := make([]route.NetID, 0, len(allIDs)) + for _, id := range allIDs { + if id != preferred { + others = append(others, id) } } - return routesToDeselect -} - -func (m *DefaultManager) deselectExitNodes(routesToDeselect []route.NetID) { - if len(routesToDeselect) == 0 { - return + if len(others) > 0 { + if err := m.routeSelector.DeselectRoutes(others, allIDs); err != nil { + log.Warnf("deselect other exit nodes: %v", err) + } } - - err := m.routeSelector.DeselectRoutes(routesToDeselect, routesToDeselect) - if err != nil { - log.Warnf("Failed to deselect exit nodes: %v", err) + if preferred != "" { + if err := m.routeSelector.SelectRoutes([]route.NetID{preferred}, true, allIDs); err != nil { + log.Warnf("select preferred exit node %q: %v", preferred, err) + } } } -func (m *DefaultManager) selectExitNodesByManagement(selectedByManagement []route.NetID, allIDs []route.NetID) { - if len(selectedByManagement) == 0 { - return - } - - err := m.routeSelector.SelectRoutes(selectedByManagement, true, allIDs) - if err != nil { - log.Warnf("Failed to select exit nodes: %v", err) - } +func (m *DefaultManager) logExitNodeUpdate(info exitNodeInfo, preferred route.NetID) { + log.Debugf("Exit node selection: %d available, preferred=%q (%d user-selected, %d user-deselected, %d management-selected)", + len(info.allIDs), preferred, len(info.userSelected), len(info.userDeselected), len(info.selectedByManagement)) } -func (m *DefaultManager) logExitNodeUpdate(info exitNodeInfo) { - log.Debugf("Updated route selector: %d exit nodes available, %d selected by management, %d user-selected, %d user-deselected", - len(info.allIDs), len(info.selectedByManagement), len(info.userSelected), len(info.userDeselected)) +// minNetID returns the lexicographically smallest NetID, for a deterministic +// default pick that stays stable across restarts. +func minNetID(ids []route.NetID) route.NetID { + if len(ids) == 0 { + return "" + } + best := ids[0] + for _, id := range ids[1:] { + if id < best { + best = id + } + } + return best } diff --git a/client/internal/routeselector/routeselector.go b/client/internal/routeselector/routeselector.go index 232baf746..3d2b19ba7 100644 --- a/client/internal/routeselector/routeselector.go +++ b/client/internal/routeselector/routeselector.go @@ -115,7 +115,9 @@ func (rs *RouteSelector) DeselectAllRoutes() { clear(rs.selectedRoutes) } -// IsDeselectAll reports whether the user has explicitly deselected all routes. +// IsDeselectAll reports whether the global "deselect all" flag is set, i.e. the +// user explicitly disabled every route. Callers enforcing per-route invariants +// (e.g. single exit node) should leave the selection untouched when it is. func (rs *RouteSelector) IsDeselectAll() bool { rs.mu.RLock() defer rs.mu.RUnlock() diff --git a/client/internal/routeselector/routeselector_test.go b/client/internal/routeselector/routeselector_test.go index c9d6acb4d..2b1ba3fb9 100644 --- a/client/internal/routeselector/routeselector_test.go +++ b/client/internal/routeselector/routeselector_test.go @@ -859,3 +859,31 @@ func TestRouteSelector_ComplexScenarios(t *testing.T) { }) } } + +// TestRouteSelector_EnableExitNodeKeepsOtherRoutes is a regression test for the +// tray exit-node toggle disabling every non-exit routed network. The tray used +// to Select an exit node with append=false, which the RouteSelector treats as +// "drop the whole current selection" (default-on semantics) — so enabling an +// exit node also turned off every LAN/route the user had on. The fix sends +// append=true and lets the daemon's SelectNetworks handler deselect only the +// sibling exit nodes. This test models that handler sequence against the +// selector: SelectRoutes(exit, append=true) followed by DeselectRoutes(other +// exit nodes) must leave non-exit routes untouched. +func TestRouteSelector_EnableExitNodeKeepsOtherRoutes(t *testing.T) { + rs := routeselector.NewRouteSelector() + all := []route.NetID{"exitA", "exitB", "lan1", "lan2"} + + // User has two LAN routes on (default-on: nothing deselected => all selected). + require.True(t, rs.IsSelected("lan1")) + require.True(t, rs.IsSelected("lan2")) + + // Tray enables exitA: SelectNetworks handler does SelectRoutes(append=true) + // then deselects sibling exit nodes (exitB), never the LAN routes. + require.NoError(t, rs.SelectRoutes([]route.NetID{"exitA"}, true, all)) + require.NoError(t, rs.DeselectRoutes([]route.NetID{"exitB"}, all)) + + assert.True(t, rs.IsSelected("exitA"), "selected exit node stays on") + assert.False(t, rs.IsSelected("exitB"), "sibling exit node is deselected") + assert.True(t, rs.IsSelected("lan1"), "non-exit route must stay selected") + assert.True(t, rs.IsSelected("lan2"), "non-exit route must stay selected") +} diff --git a/client/internal/state.go b/client/internal/state.go index 041cb73f8..0adfa26e4 100644 --- a/client/internal/state.go +++ b/client/internal/state.go @@ -33,17 +33,34 @@ func CtxGetState(ctx context.Context) *contextState { } type contextState struct { - err error - status StatusType - mutex sync.Mutex + err error + status StatusType + mutex sync.Mutex + onChange func() +} + +// SetOnChange installs a callback fired after every successful Set. Used by +// the daemon to wire the status recorder's notifyStateChange so any +// state.Set in the connect/login paths pushes a fresh snapshot to +// SubscribeStatus subscribers without each callsite having to opt in. +// The callback runs outside the contextState mutex to avoid a lock-order +// dependency with the recorder's stateChangeMux. +func (c *contextState) SetOnChange(fn func()) { + c.mutex.Lock() + c.onChange = fn + c.mutex.Unlock() } func (c *contextState) Set(update StatusType) { c.mutex.Lock() - defer c.mutex.Unlock() - c.status = update c.err = nil + cb := c.onChange + c.mutex.Unlock() + + if cb != nil { + cb() + } } func (c *contextState) Status() (StatusType, error) { @@ -57,6 +74,17 @@ func (c *contextState) Status() (StatusType, error) { return c.status, nil } +// CurrentStatus returns the last status set via Set, ignoring any wrapped +// error. Use when the status is needed for reporting purposes (e.g. the +// status snapshot stream) and a transient wrapped error from a retry loop +// shouldn't blank out the underlying status. +func (c *contextState) CurrentStatus() StatusType { + c.mutex.Lock() + defer c.mutex.Unlock() + + return c.status +} + func (c *contextState) Wrap(err error) error { c.mutex.Lock() defer c.mutex.Unlock() diff --git a/client/mdm/canonical_loaders.go b/client/mdm/canonical_loaders.go index 6e7ab19cb..d4a8234b2 100644 --- a/client/mdm/canonical_loaders.go +++ b/client/mdm/canonical_loaders.go @@ -15,6 +15,7 @@ var allKeys = []string{ KeyDisableUpdateSettings, KeyDisableProfiles, KeyDisableNetworks, + KeyDisableAdvancedView, KeyDisableClientRoutes, KeyDisableServerRoutes, KeyBlockInbound, diff --git a/client/mdm/policy.go b/client/mdm/policy.go index 109fb322e..e7da6e7f1 100644 --- a/client/mdm/policy.go +++ b/client/mdm/policy.go @@ -23,6 +23,13 @@ const ( KeyDisableUpdateSettings = "disableUpdateSettings" KeyDisableProfiles = "disableProfiles" KeyDisableNetworks = "disableNetworks" + // KeyDisableAdvancedView gates the advanced-view section in the + // upcoming UI revision. UI-only: NOT stored on Config, not + // applied by applyMDMPolicy, not rejectable via SetConfig. The + // daemon surfaces it through GetFeatures (tristate: present + // true / present false / absent) and the same key appears in + // GetConfigResponse.mDMManagedFields when set. + KeyDisableAdvancedView = "disableAdvancedView" KeyDisableClientRoutes = "disableClientRoutes" KeyDisableServerRoutes = "disableServerRoutes" KeyBlockInbound = "blockInbound" diff --git a/client/netbird.wxs b/client/netbird.wxs index 6f18b63b5..79eba575a 100644 --- a/client/netbird.wxs +++ b/client/netbird.wxs @@ -32,9 +32,6 @@ - - - + + @@ -85,10 +90,40 @@ + + + + + + + + + + + + + + + - + diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 488b0186c..6fbb09958 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -192,7 +192,7 @@ func (x SystemEvent_Severity) Number() protoreflect.EnumNumber { // Deprecated: Use SystemEvent_Severity.Descriptor instead. func (SystemEvent_Severity) EnumDescriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{51, 0} + return file_daemon_proto_rawDescGZIP(), []int{53, 0} } type SystemEvent_Category int32 @@ -247,7 +247,7 @@ func (x SystemEvent_Category) Number() protoreflect.EnumNumber { // Deprecated: Use SystemEvent_Category.Descriptor instead. func (SystemEvent_Category) EnumDescriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{51, 1} + return file_daemon_proto_rawDescGZIP(), []int{53, 1} } type EmptyRequest struct { @@ -823,9 +823,15 @@ func (x *WaitSSOLoginResponse) GetEmail() string { } type UpRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` - Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` + Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` + // async instructs the daemon to start the connection attempt and return + // immediately without waiting for the engine to become ready. Status updates + // are delivered via the SubscribeStatus stream. When false (the default) the + // RPC blocks until the engine is running or gives up, which is the behaviour + // needed by the CLI. + Async bool `protobuf:"varint,4,opt,name=async,proto3" json:"async,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -874,6 +880,13 @@ func (x *UpRequest) GetUsername() string { return "" } +func (x *UpRequest) GetAsync() bool { + if x != nil { + return x.Async + } + return false +} + type UpResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -978,8 +991,12 @@ type StatusResponse struct { FullStatus *FullStatus `protobuf:"bytes,2,opt,name=fullStatus,proto3" json:"fullStatus,omitempty"` // NetBird daemon version DaemonVersion string `protobuf:"bytes,3,opt,name=daemonVersion,proto3" json:"daemonVersion,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Absolute UTC instant at which the peer's SSO session expires. + // Unset when the peer is not SSO-registered or login expiration is disabled. + // The UI derives "warning active" from this value and its own clock. + SessionExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=sessionExpiresAt,proto3" json:"sessionExpiresAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StatusResponse) Reset() { @@ -1033,6 +1050,13 @@ func (x *StatusResponse) GetDaemonVersion() string { return "" } +func (x *StatusResponse) GetSessionExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.SessionExpiresAt + } + return nil +} + type DownRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -2129,8 +2153,13 @@ type FullStatus struct { Events []*SystemEvent `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"` LazyConnectionEnabled bool `protobuf:"varint,9,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` SshServerState *SSHServerState `protobuf:"bytes,10,opt,name=sshServerState,proto3" json:"sshServerState,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // networksRevision bumps whenever the set of routed networks (route and + // exit-node candidates) or their selected state changes. The UI fingerprints + // on it to know when to re-fetch ListNetworks via the push stream, instead + // of polling on every status snapshot. + NetworksRevision uint64 `protobuf:"varint,11,opt,name=networksRevision,proto3" json:"networksRevision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FullStatus) Reset() { @@ -2233,6 +2262,13 @@ func (x *FullStatus) GetSshServerState() *SSHServerState { return nil } +func (x *FullStatus) GetNetworksRevision() uint64 { + if x != nil { + return x.NetworksRevision + } + return 0 +} + // Networks type ListNetworksRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3030,6 +3066,86 @@ func (*SetLogLevelResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{36} } +type RegisterUILogRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterUILogRequest) Reset() { + *x = RegisterUILogRequest{} + mi := &file_daemon_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterUILogRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterUILogRequest) ProtoMessage() {} + +func (x *RegisterUILogRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterUILogRequest.ProtoReflect.Descriptor instead. +func (*RegisterUILogRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{37} +} + +func (x *RegisterUILogRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type RegisterUILogResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterUILogResponse) Reset() { + *x = RegisterUILogResponse{} + mi := &file_daemon_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterUILogResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterUILogResponse) ProtoMessage() {} + +func (x *RegisterUILogResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterUILogResponse.ProtoReflect.Descriptor instead. +func (*RegisterUILogResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{38} +} + // State represents a daemon state entry type State struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3040,7 +3156,7 @@ type State struct { func (x *State) Reset() { *x = State{} - mi := &file_daemon_proto_msgTypes[37] + mi := &file_daemon_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3052,7 +3168,7 @@ func (x *State) String() string { func (*State) ProtoMessage() {} func (x *State) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[37] + mi := &file_daemon_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3065,7 +3181,7 @@ func (x *State) ProtoReflect() protoreflect.Message { // Deprecated: Use State.ProtoReflect.Descriptor instead. func (*State) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{37} + return file_daemon_proto_rawDescGZIP(), []int{39} } func (x *State) GetName() string { @@ -3084,7 +3200,7 @@ type ListStatesRequest struct { func (x *ListStatesRequest) Reset() { *x = ListStatesRequest{} - mi := &file_daemon_proto_msgTypes[38] + mi := &file_daemon_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3096,7 +3212,7 @@ func (x *ListStatesRequest) String() string { func (*ListStatesRequest) ProtoMessage() {} func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[38] + mi := &file_daemon_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3109,7 +3225,7 @@ func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStatesRequest.ProtoReflect.Descriptor instead. func (*ListStatesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{38} + return file_daemon_proto_rawDescGZIP(), []int{40} } // ListStatesResponse contains a list of states @@ -3122,7 +3238,7 @@ type ListStatesResponse struct { func (x *ListStatesResponse) Reset() { *x = ListStatesResponse{} - mi := &file_daemon_proto_msgTypes[39] + mi := &file_daemon_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3134,7 +3250,7 @@ func (x *ListStatesResponse) String() string { func (*ListStatesResponse) ProtoMessage() {} func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[39] + mi := &file_daemon_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3147,7 +3263,7 @@ func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStatesResponse.ProtoReflect.Descriptor instead. func (*ListStatesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{39} + return file_daemon_proto_rawDescGZIP(), []int{41} } func (x *ListStatesResponse) GetStates() []*State { @@ -3168,7 +3284,7 @@ type CleanStateRequest struct { func (x *CleanStateRequest) Reset() { *x = CleanStateRequest{} - mi := &file_daemon_proto_msgTypes[40] + mi := &file_daemon_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3180,7 +3296,7 @@ func (x *CleanStateRequest) String() string { func (*CleanStateRequest) ProtoMessage() {} func (x *CleanStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[40] + mi := &file_daemon_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3193,7 +3309,7 @@ func (x *CleanStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CleanStateRequest.ProtoReflect.Descriptor instead. func (*CleanStateRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{40} + return file_daemon_proto_rawDescGZIP(), []int{42} } func (x *CleanStateRequest) GetStateName() string { @@ -3220,7 +3336,7 @@ type CleanStateResponse struct { func (x *CleanStateResponse) Reset() { *x = CleanStateResponse{} - mi := &file_daemon_proto_msgTypes[41] + mi := &file_daemon_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3232,7 +3348,7 @@ func (x *CleanStateResponse) String() string { func (*CleanStateResponse) ProtoMessage() {} func (x *CleanStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[41] + mi := &file_daemon_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3245,7 +3361,7 @@ func (x *CleanStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CleanStateResponse.ProtoReflect.Descriptor instead. func (*CleanStateResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{41} + return file_daemon_proto_rawDescGZIP(), []int{43} } func (x *CleanStateResponse) GetCleanedStates() int32 { @@ -3266,7 +3382,7 @@ type DeleteStateRequest struct { func (x *DeleteStateRequest) Reset() { *x = DeleteStateRequest{} - mi := &file_daemon_proto_msgTypes[42] + mi := &file_daemon_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3278,7 +3394,7 @@ func (x *DeleteStateRequest) String() string { func (*DeleteStateRequest) ProtoMessage() {} func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[42] + mi := &file_daemon_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3291,7 +3407,7 @@ func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteStateRequest.ProtoReflect.Descriptor instead. func (*DeleteStateRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{42} + return file_daemon_proto_rawDescGZIP(), []int{44} } func (x *DeleteStateRequest) GetStateName() string { @@ -3318,7 +3434,7 @@ type DeleteStateResponse struct { func (x *DeleteStateResponse) Reset() { *x = DeleteStateResponse{} - mi := &file_daemon_proto_msgTypes[43] + mi := &file_daemon_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3330,7 +3446,7 @@ func (x *DeleteStateResponse) String() string { func (*DeleteStateResponse) ProtoMessage() {} func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[43] + mi := &file_daemon_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3343,7 +3459,7 @@ func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteStateResponse.ProtoReflect.Descriptor instead. func (*DeleteStateResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{43} + return file_daemon_proto_rawDescGZIP(), []int{45} } func (x *DeleteStateResponse) GetDeletedStates() int32 { @@ -3362,7 +3478,7 @@ type SetSyncResponsePersistenceRequest struct { func (x *SetSyncResponsePersistenceRequest) Reset() { *x = SetSyncResponsePersistenceRequest{} - mi := &file_daemon_proto_msgTypes[44] + mi := &file_daemon_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3374,7 +3490,7 @@ func (x *SetSyncResponsePersistenceRequest) String() string { func (*SetSyncResponsePersistenceRequest) ProtoMessage() {} func (x *SetSyncResponsePersistenceRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[44] + mi := &file_daemon_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3387,7 +3503,7 @@ func (x *SetSyncResponsePersistenceRequest) ProtoReflect() protoreflect.Message // Deprecated: Use SetSyncResponsePersistenceRequest.ProtoReflect.Descriptor instead. func (*SetSyncResponsePersistenceRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{44} + return file_daemon_proto_rawDescGZIP(), []int{46} } func (x *SetSyncResponsePersistenceRequest) GetEnabled() bool { @@ -3405,7 +3521,7 @@ type SetSyncResponsePersistenceResponse struct { func (x *SetSyncResponsePersistenceResponse) Reset() { *x = SetSyncResponsePersistenceResponse{} - mi := &file_daemon_proto_msgTypes[45] + mi := &file_daemon_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3417,7 +3533,7 @@ func (x *SetSyncResponsePersistenceResponse) String() string { func (*SetSyncResponsePersistenceResponse) ProtoMessage() {} func (x *SetSyncResponsePersistenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[45] + mi := &file_daemon_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3430,7 +3546,7 @@ func (x *SetSyncResponsePersistenceResponse) ProtoReflect() protoreflect.Message // Deprecated: Use SetSyncResponsePersistenceResponse.ProtoReflect.Descriptor instead. func (*SetSyncResponsePersistenceResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{45} + return file_daemon_proto_rawDescGZIP(), []int{47} } type TCPFlags struct { @@ -3447,7 +3563,7 @@ type TCPFlags struct { func (x *TCPFlags) Reset() { *x = TCPFlags{} - mi := &file_daemon_proto_msgTypes[46] + mi := &file_daemon_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3459,7 +3575,7 @@ func (x *TCPFlags) String() string { func (*TCPFlags) ProtoMessage() {} func (x *TCPFlags) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[46] + mi := &file_daemon_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3472,7 +3588,7 @@ func (x *TCPFlags) ProtoReflect() protoreflect.Message { // Deprecated: Use TCPFlags.ProtoReflect.Descriptor instead. func (*TCPFlags) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{46} + return file_daemon_proto_rawDescGZIP(), []int{48} } func (x *TCPFlags) GetSyn() bool { @@ -3534,7 +3650,7 @@ type TracePacketRequest struct { func (x *TracePacketRequest) Reset() { *x = TracePacketRequest{} - mi := &file_daemon_proto_msgTypes[47] + mi := &file_daemon_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3546,7 +3662,7 @@ func (x *TracePacketRequest) String() string { func (*TracePacketRequest) ProtoMessage() {} func (x *TracePacketRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[47] + mi := &file_daemon_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3559,7 +3675,7 @@ func (x *TracePacketRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TracePacketRequest.ProtoReflect.Descriptor instead. func (*TracePacketRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{47} + return file_daemon_proto_rawDescGZIP(), []int{49} } func (x *TracePacketRequest) GetSourceIp() string { @@ -3637,7 +3753,7 @@ type TraceStage struct { func (x *TraceStage) Reset() { *x = TraceStage{} - mi := &file_daemon_proto_msgTypes[48] + mi := &file_daemon_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3649,7 +3765,7 @@ func (x *TraceStage) String() string { func (*TraceStage) ProtoMessage() {} func (x *TraceStage) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[48] + mi := &file_daemon_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3662,7 +3778,7 @@ func (x *TraceStage) ProtoReflect() protoreflect.Message { // Deprecated: Use TraceStage.ProtoReflect.Descriptor instead. func (*TraceStage) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{48} + return file_daemon_proto_rawDescGZIP(), []int{50} } func (x *TraceStage) GetName() string { @@ -3703,7 +3819,7 @@ type TracePacketResponse struct { func (x *TracePacketResponse) Reset() { *x = TracePacketResponse{} - mi := &file_daemon_proto_msgTypes[49] + mi := &file_daemon_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3715,7 +3831,7 @@ func (x *TracePacketResponse) String() string { func (*TracePacketResponse) ProtoMessage() {} func (x *TracePacketResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[49] + mi := &file_daemon_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3728,7 +3844,7 @@ func (x *TracePacketResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TracePacketResponse.ProtoReflect.Descriptor instead. func (*TracePacketResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{49} + return file_daemon_proto_rawDescGZIP(), []int{51} } func (x *TracePacketResponse) GetStages() []*TraceStage { @@ -3753,7 +3869,7 @@ type SubscribeRequest struct { func (x *SubscribeRequest) Reset() { *x = SubscribeRequest{} - mi := &file_daemon_proto_msgTypes[50] + mi := &file_daemon_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3765,7 +3881,7 @@ func (x *SubscribeRequest) String() string { func (*SubscribeRequest) ProtoMessage() {} func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[50] + mi := &file_daemon_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3778,7 +3894,7 @@ func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeRequest.ProtoReflect.Descriptor instead. func (*SubscribeRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{50} + return file_daemon_proto_rawDescGZIP(), []int{52} } type SystemEvent struct { @@ -3796,7 +3912,7 @@ type SystemEvent struct { func (x *SystemEvent) Reset() { *x = SystemEvent{} - mi := &file_daemon_proto_msgTypes[51] + mi := &file_daemon_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3808,7 +3924,7 @@ func (x *SystemEvent) String() string { func (*SystemEvent) ProtoMessage() {} func (x *SystemEvent) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[51] + mi := &file_daemon_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3821,7 +3937,7 @@ func (x *SystemEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemEvent.ProtoReflect.Descriptor instead. func (*SystemEvent) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{51} + return file_daemon_proto_rawDescGZIP(), []int{53} } func (x *SystemEvent) GetId() string { @@ -3881,7 +3997,7 @@ type GetEventsRequest struct { func (x *GetEventsRequest) Reset() { *x = GetEventsRequest{} - mi := &file_daemon_proto_msgTypes[52] + mi := &file_daemon_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3893,7 +4009,7 @@ func (x *GetEventsRequest) String() string { func (*GetEventsRequest) ProtoMessage() {} func (x *GetEventsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[52] + mi := &file_daemon_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3906,7 +4022,7 @@ func (x *GetEventsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEventsRequest.ProtoReflect.Descriptor instead. func (*GetEventsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{52} + return file_daemon_proto_rawDescGZIP(), []int{54} } type GetEventsResponse struct { @@ -3918,7 +4034,7 @@ type GetEventsResponse struct { func (x *GetEventsResponse) Reset() { *x = GetEventsResponse{} - mi := &file_daemon_proto_msgTypes[53] + mi := &file_daemon_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3930,7 +4046,7 @@ func (x *GetEventsResponse) String() string { func (*GetEventsResponse) ProtoMessage() {} func (x *GetEventsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[53] + mi := &file_daemon_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3943,7 +4059,7 @@ func (x *GetEventsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEventsResponse.ProtoReflect.Descriptor instead. func (*GetEventsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{53} + return file_daemon_proto_rawDescGZIP(), []int{55} } func (x *GetEventsResponse) GetEvents() []*SystemEvent { @@ -3965,7 +4081,7 @@ type SwitchProfileRequest struct { func (x *SwitchProfileRequest) Reset() { *x = SwitchProfileRequest{} - mi := &file_daemon_proto_msgTypes[54] + mi := &file_daemon_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3977,7 +4093,7 @@ func (x *SwitchProfileRequest) String() string { func (*SwitchProfileRequest) ProtoMessage() {} func (x *SwitchProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[54] + mi := &file_daemon_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3990,7 +4106,7 @@ func (x *SwitchProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SwitchProfileRequest.ProtoReflect.Descriptor instead. func (*SwitchProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{54} + return file_daemon_proto_rawDescGZIP(), []int{56} } func (x *SwitchProfileRequest) GetProfileName() string { @@ -4019,7 +4135,7 @@ type SwitchProfileResponse struct { func (x *SwitchProfileResponse) Reset() { *x = SwitchProfileResponse{} - mi := &file_daemon_proto_msgTypes[55] + mi := &file_daemon_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4031,7 +4147,7 @@ func (x *SwitchProfileResponse) String() string { func (*SwitchProfileResponse) ProtoMessage() {} func (x *SwitchProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[55] + mi := &file_daemon_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4044,7 +4160,7 @@ func (x *SwitchProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SwitchProfileResponse.ProtoReflect.Descriptor instead. func (*SwitchProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{55} + return file_daemon_proto_rawDescGZIP(), []int{57} } func (x *SwitchProfileResponse) GetId() string { @@ -4100,7 +4216,7 @@ type SetConfigRequest struct { func (x *SetConfigRequest) Reset() { *x = SetConfigRequest{} - mi := &file_daemon_proto_msgTypes[56] + mi := &file_daemon_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4112,7 +4228,7 @@ func (x *SetConfigRequest) String() string { func (*SetConfigRequest) ProtoMessage() {} func (x *SetConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[56] + mi := &file_daemon_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4125,7 +4241,7 @@ func (x *SetConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetConfigRequest.ProtoReflect.Descriptor instead. func (*SetConfigRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{56} + return file_daemon_proto_rawDescGZIP(), []int{58} } func (x *SetConfigRequest) GetUsername() string { @@ -4381,7 +4497,7 @@ type SetConfigResponse struct { func (x *SetConfigResponse) Reset() { *x = SetConfigResponse{} - mi := &file_daemon_proto_msgTypes[57] + mi := &file_daemon_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4393,7 +4509,7 @@ func (x *SetConfigResponse) String() string { func (*SetConfigResponse) ProtoMessage() {} func (x *SetConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[57] + mi := &file_daemon_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4406,7 +4522,7 @@ func (x *SetConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetConfigResponse.ProtoReflect.Descriptor instead. func (*SetConfigResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{57} + return file_daemon_proto_rawDescGZIP(), []int{59} } type AddProfileRequest struct { @@ -4421,7 +4537,7 @@ type AddProfileRequest struct { func (x *AddProfileRequest) Reset() { *x = AddProfileRequest{} - mi := &file_daemon_proto_msgTypes[58] + mi := &file_daemon_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4433,7 +4549,7 @@ func (x *AddProfileRequest) String() string { func (*AddProfileRequest) ProtoMessage() {} func (x *AddProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[58] + mi := &file_daemon_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4446,7 +4562,7 @@ func (x *AddProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddProfileRequest.ProtoReflect.Descriptor instead. func (*AddProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{58} + return file_daemon_proto_rawDescGZIP(), []int{60} } func (x *AddProfileRequest) GetUsername() string { @@ -4474,7 +4590,7 @@ type AddProfileResponse struct { func (x *AddProfileResponse) Reset() { *x = AddProfileResponse{} - mi := &file_daemon_proto_msgTypes[59] + mi := &file_daemon_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4486,7 +4602,7 @@ func (x *AddProfileResponse) String() string { func (*AddProfileResponse) ProtoMessage() {} func (x *AddProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[59] + mi := &file_daemon_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4499,7 +4615,7 @@ func (x *AddProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddProfileResponse.ProtoReflect.Descriptor instead. func (*AddProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{59} + return file_daemon_proto_rawDescGZIP(), []int{61} } func (x *AddProfileResponse) GetId() string { @@ -4522,7 +4638,7 @@ type RenameProfileRequest struct { func (x *RenameProfileRequest) Reset() { *x = RenameProfileRequest{} - mi := &file_daemon_proto_msgTypes[60] + mi := &file_daemon_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4534,7 +4650,7 @@ func (x *RenameProfileRequest) String() string { func (*RenameProfileRequest) ProtoMessage() {} func (x *RenameProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[60] + mi := &file_daemon_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4547,7 +4663,7 @@ func (x *RenameProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenameProfileRequest.ProtoReflect.Descriptor instead. func (*RenameProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{60} + return file_daemon_proto_rawDescGZIP(), []int{62} } func (x *RenameProfileRequest) GetUsername() string { @@ -4581,7 +4697,7 @@ type RenameProfileResponse struct { func (x *RenameProfileResponse) Reset() { *x = RenameProfileResponse{} - mi := &file_daemon_proto_msgTypes[61] + mi := &file_daemon_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4593,7 +4709,7 @@ func (x *RenameProfileResponse) String() string { func (*RenameProfileResponse) ProtoMessage() {} func (x *RenameProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[61] + mi := &file_daemon_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4606,7 +4722,7 @@ func (x *RenameProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenameProfileResponse.ProtoReflect.Descriptor instead. func (*RenameProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{61} + return file_daemon_proto_rawDescGZIP(), []int{63} } func (x *RenameProfileResponse) GetOldProfileName() string { @@ -4628,7 +4744,7 @@ type RemoveProfileRequest struct { func (x *RemoveProfileRequest) Reset() { *x = RemoveProfileRequest{} - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4640,7 +4756,7 @@ func (x *RemoveProfileRequest) String() string { func (*RemoveProfileRequest) ProtoMessage() {} func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4653,7 +4769,7 @@ func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveProfileRequest.ProtoReflect.Descriptor instead. func (*RemoveProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{62} + return file_daemon_proto_rawDescGZIP(), []int{64} } func (x *RemoveProfileRequest) GetUsername() string { @@ -4681,7 +4797,7 @@ type RemoveProfileResponse struct { func (x *RemoveProfileResponse) Reset() { *x = RemoveProfileResponse{} - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4693,7 +4809,7 @@ func (x *RemoveProfileResponse) String() string { func (*RemoveProfileResponse) ProtoMessage() {} func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4706,7 +4822,7 @@ func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveProfileResponse.ProtoReflect.Descriptor instead. func (*RemoveProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{63} + return file_daemon_proto_rawDescGZIP(), []int{65} } func (x *RemoveProfileResponse) GetId() string { @@ -4725,7 +4841,7 @@ type ListProfilesRequest struct { func (x *ListProfilesRequest) Reset() { *x = ListProfilesRequest{} - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4737,7 +4853,7 @@ func (x *ListProfilesRequest) String() string { func (*ListProfilesRequest) ProtoMessage() {} func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4750,7 +4866,7 @@ func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProfilesRequest.ProtoReflect.Descriptor instead. func (*ListProfilesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{64} + return file_daemon_proto_rawDescGZIP(), []int{66} } func (x *ListProfilesRequest) GetUsername() string { @@ -4769,7 +4885,7 @@ type ListProfilesResponse struct { func (x *ListProfilesResponse) Reset() { *x = ListProfilesResponse{} - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4781,7 +4897,7 @@ func (x *ListProfilesResponse) String() string { func (*ListProfilesResponse) ProtoMessage() {} func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4794,7 +4910,7 @@ func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProfilesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{65} + return file_daemon_proto_rawDescGZIP(), []int{67} } func (x *ListProfilesResponse) GetProfiles() []*Profile { @@ -4815,7 +4931,7 @@ type Profile struct { func (x *Profile) Reset() { *x = Profile{} - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4827,7 +4943,7 @@ func (x *Profile) String() string { func (*Profile) ProtoMessage() {} func (x *Profile) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4840,7 +4956,7 @@ func (x *Profile) ProtoReflect() protoreflect.Message { // Deprecated: Use Profile.ProtoReflect.Descriptor instead. func (*Profile) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{66} + return file_daemon_proto_rawDescGZIP(), []int{68} } func (x *Profile) GetName() string { @@ -4872,7 +4988,7 @@ type GetActiveProfileRequest struct { func (x *GetActiveProfileRequest) Reset() { *x = GetActiveProfileRequest{} - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4884,7 +5000,7 @@ func (x *GetActiveProfileRequest) String() string { func (*GetActiveProfileRequest) ProtoMessage() {} func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4897,7 +5013,7 @@ func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveProfileRequest.ProtoReflect.Descriptor instead. func (*GetActiveProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{67} + return file_daemon_proto_rawDescGZIP(), []int{69} } type GetActiveProfileResponse struct { @@ -4911,7 +5027,7 @@ type GetActiveProfileResponse struct { func (x *GetActiveProfileResponse) Reset() { *x = GetActiveProfileResponse{} - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4923,7 +5039,7 @@ func (x *GetActiveProfileResponse) String() string { func (*GetActiveProfileResponse) ProtoMessage() {} func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4936,7 +5052,7 @@ func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveProfileResponse.ProtoReflect.Descriptor instead. func (*GetActiveProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{68} + return file_daemon_proto_rawDescGZIP(), []int{70} } func (x *GetActiveProfileResponse) GetProfileName() string { @@ -4970,7 +5086,7 @@ type LogoutRequest struct { func (x *LogoutRequest) Reset() { *x = LogoutRequest{} - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4982,7 +5098,7 @@ func (x *LogoutRequest) String() string { func (*LogoutRequest) ProtoMessage() {} func (x *LogoutRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4995,7 +5111,7 @@ func (x *LogoutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. func (*LogoutRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{69} + return file_daemon_proto_rawDescGZIP(), []int{71} } func (x *LogoutRequest) GetProfileName() string { @@ -5020,7 +5136,7 @@ type LogoutResponse struct { func (x *LogoutResponse) Reset() { *x = LogoutResponse{} - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5032,7 +5148,7 @@ func (x *LogoutResponse) String() string { func (*LogoutResponse) ProtoMessage() {} func (x *LogoutResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5045,7 +5161,79 @@ func (x *LogoutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutResponse.ProtoReflect.Descriptor instead. func (*LogoutResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{70} + return file_daemon_proto_rawDescGZIP(), []int{72} +} + +type WailsUIReadyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WailsUIReadyRequest) Reset() { + *x = WailsUIReadyRequest{} + mi := &file_daemon_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WailsUIReadyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WailsUIReadyRequest) ProtoMessage() {} + +func (x *WailsUIReadyRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[73] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WailsUIReadyRequest.ProtoReflect.Descriptor instead. +func (*WailsUIReadyRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{73} +} + +type WailsUIReadyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WailsUIReadyResponse) Reset() { + *x = WailsUIReadyResponse{} + mi := &file_daemon_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WailsUIReadyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WailsUIReadyResponse) ProtoMessage() {} + +func (x *WailsUIReadyResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[74] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WailsUIReadyResponse.ProtoReflect.Descriptor instead. +func (*WailsUIReadyResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{74} } type GetFeaturesRequest struct { @@ -5056,7 +5244,7 @@ type GetFeaturesRequest struct { func (x *GetFeaturesRequest) Reset() { *x = GetFeaturesRequest{} - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5068,7 +5256,7 @@ func (x *GetFeaturesRequest) String() string { func (*GetFeaturesRequest) ProtoMessage() {} func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5081,7 +5269,7 @@ func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeaturesRequest.ProtoReflect.Descriptor instead. func (*GetFeaturesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{71} + return file_daemon_proto_rawDescGZIP(), []int{75} } type GetFeaturesResponse struct { @@ -5089,13 +5277,19 @@ type GetFeaturesResponse struct { DisableProfiles bool `protobuf:"varint,1,opt,name=disable_profiles,json=disableProfiles,proto3" json:"disable_profiles,omitempty"` DisableUpdateSettings bool `protobuf:"varint,2,opt,name=disable_update_settings,json=disableUpdateSettings,proto3" json:"disable_update_settings,omitempty"` DisableNetworks bool `protobuf:"varint,3,opt,name=disable_networks,json=disableNetworks,proto3" json:"disable_networks,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // disableAdvancedView gates the upcoming UI revision's advanced + // section. Tristate: unset = no MDM directive, the UI applies its + // own default; true = MDM enforces disable; false = MDM enforces + // enable. Sourced exclusively from the MDM policy — no CLI / + // config flag backs this value. + DisableAdvancedView *bool `protobuf:"varint,4,opt,name=disable_advanced_view,json=disableAdvancedView,proto3,oneof" json:"disable_advanced_view,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetFeaturesResponse) Reset() { *x = GetFeaturesResponse{} - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5107,7 +5301,7 @@ func (x *GetFeaturesResponse) String() string { func (*GetFeaturesResponse) ProtoMessage() {} func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5120,7 +5314,7 @@ func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeaturesResponse.ProtoReflect.Descriptor instead. func (*GetFeaturesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{72} + return file_daemon_proto_rawDescGZIP(), []int{76} } func (x *GetFeaturesResponse) GetDisableProfiles() bool { @@ -5144,6 +5338,13 @@ func (x *GetFeaturesResponse) GetDisableNetworks() bool { return false } +func (x *GetFeaturesResponse) GetDisableAdvancedView() bool { + if x != nil && x.DisableAdvancedView != nil { + return *x.DisableAdvancedView + } + return false +} + // MDMManagedFieldsViolation is attached as a gRPC error detail on a // FailedPrecondition status returned from SetConfig (and similar mutating // RPCs) when the caller tries to modify one or more MDM-enforced fields. @@ -5158,7 +5359,7 @@ type MDMManagedFieldsViolation struct { func (x *MDMManagedFieldsViolation) Reset() { *x = MDMManagedFieldsViolation{} - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5170,7 +5371,7 @@ func (x *MDMManagedFieldsViolation) String() string { func (*MDMManagedFieldsViolation) ProtoMessage() {} func (x *MDMManagedFieldsViolation) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5183,7 +5384,7 @@ func (x *MDMManagedFieldsViolation) ProtoReflect() protoreflect.Message { // Deprecated: Use MDMManagedFieldsViolation.ProtoReflect.Descriptor instead. func (*MDMManagedFieldsViolation) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{73} + return file_daemon_proto_rawDescGZIP(), []int{77} } func (x *MDMManagedFieldsViolation) GetFields() []string { @@ -5201,7 +5402,7 @@ type TriggerUpdateRequest struct { func (x *TriggerUpdateRequest) Reset() { *x = TriggerUpdateRequest{} - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5213,7 +5414,7 @@ func (x *TriggerUpdateRequest) String() string { func (*TriggerUpdateRequest) ProtoMessage() {} func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5226,7 +5427,7 @@ func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TriggerUpdateRequest.ProtoReflect.Descriptor instead. func (*TriggerUpdateRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{74} + return file_daemon_proto_rawDescGZIP(), []int{78} } type TriggerUpdateResponse struct { @@ -5239,7 +5440,7 @@ type TriggerUpdateResponse struct { func (x *TriggerUpdateResponse) Reset() { *x = TriggerUpdateResponse{} - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5251,7 +5452,7 @@ func (x *TriggerUpdateResponse) String() string { func (*TriggerUpdateResponse) ProtoMessage() {} func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5264,7 +5465,7 @@ func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TriggerUpdateResponse.ProtoReflect.Descriptor instead. func (*TriggerUpdateResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{75} + return file_daemon_proto_rawDescGZIP(), []int{79} } func (x *TriggerUpdateResponse) GetSuccess() bool { @@ -5292,7 +5493,7 @@ type GetPeerSSHHostKeyRequest struct { func (x *GetPeerSSHHostKeyRequest) Reset() { *x = GetPeerSSHHostKeyRequest{} - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5304,7 +5505,7 @@ func (x *GetPeerSSHHostKeyRequest) String() string { func (*GetPeerSSHHostKeyRequest) ProtoMessage() {} func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5317,7 +5518,7 @@ func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPeerSSHHostKeyRequest.ProtoReflect.Descriptor instead. func (*GetPeerSSHHostKeyRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{76} + return file_daemon_proto_rawDescGZIP(), []int{80} } func (x *GetPeerSSHHostKeyRequest) GetPeerAddress() string { @@ -5344,7 +5545,7 @@ type GetPeerSSHHostKeyResponse struct { func (x *GetPeerSSHHostKeyResponse) Reset() { *x = GetPeerSSHHostKeyResponse{} - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5356,7 +5557,7 @@ func (x *GetPeerSSHHostKeyResponse) String() string { func (*GetPeerSSHHostKeyResponse) ProtoMessage() {} func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5369,7 +5570,7 @@ func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPeerSSHHostKeyResponse.ProtoReflect.Descriptor instead. func (*GetPeerSSHHostKeyResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{77} + return file_daemon_proto_rawDescGZIP(), []int{81} } func (x *GetPeerSSHHostKeyResponse) GetSshHostKey() []byte { @@ -5411,7 +5612,7 @@ type RequestJWTAuthRequest struct { func (x *RequestJWTAuthRequest) Reset() { *x = RequestJWTAuthRequest{} - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5423,7 +5624,7 @@ func (x *RequestJWTAuthRequest) String() string { func (*RequestJWTAuthRequest) ProtoMessage() {} func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5436,7 +5637,7 @@ func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestJWTAuthRequest.ProtoReflect.Descriptor instead. func (*RequestJWTAuthRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{78} + return file_daemon_proto_rawDescGZIP(), []int{82} } func (x *RequestJWTAuthRequest) GetHint() string { @@ -5469,7 +5670,7 @@ type RequestJWTAuthResponse struct { func (x *RequestJWTAuthResponse) Reset() { *x = RequestJWTAuthResponse{} - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5481,7 +5682,7 @@ func (x *RequestJWTAuthResponse) String() string { func (*RequestJWTAuthResponse) ProtoMessage() {} func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5494,7 +5695,7 @@ func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestJWTAuthResponse.ProtoReflect.Descriptor instead. func (*RequestJWTAuthResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{79} + return file_daemon_proto_rawDescGZIP(), []int{83} } func (x *RequestJWTAuthResponse) GetVerificationURI() string { @@ -5559,7 +5760,7 @@ type WaitJWTTokenRequest struct { func (x *WaitJWTTokenRequest) Reset() { *x = WaitJWTTokenRequest{} - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5571,7 +5772,7 @@ func (x *WaitJWTTokenRequest) String() string { func (*WaitJWTTokenRequest) ProtoMessage() {} func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5584,7 +5785,7 @@ func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitJWTTokenRequest.ProtoReflect.Descriptor instead. func (*WaitJWTTokenRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{80} + return file_daemon_proto_rawDescGZIP(), []int{84} } func (x *WaitJWTTokenRequest) GetDeviceCode() string { @@ -5616,7 +5817,7 @@ type WaitJWTTokenResponse struct { func (x *WaitJWTTokenResponse) Reset() { *x = WaitJWTTokenResponse{} - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5628,7 +5829,7 @@ func (x *WaitJWTTokenResponse) String() string { func (*WaitJWTTokenResponse) ProtoMessage() {} func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5641,7 +5842,7 @@ func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitJWTTokenResponse.ProtoReflect.Descriptor instead. func (*WaitJWTTokenResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{81} + return file_daemon_proto_rawDescGZIP(), []int{85} } func (x *WaitJWTTokenResponse) GetToken() string { @@ -5665,6 +5866,318 @@ func (x *WaitJWTTokenResponse) GetExpiresIn() int64 { return 0 } +// RequestExtendAuthSessionRequest kicks off the session-extension SSO flow. +type RequestExtendAuthSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional OIDC login_hint (typically the user's email) to pre-fill the + // IdP login form. + Hint *string `protobuf:"bytes,1,opt,name=hint,proto3,oneof" json:"hint,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestExtendAuthSessionRequest) Reset() { + *x = RequestExtendAuthSessionRequest{} + mi := &file_daemon_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestExtendAuthSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestExtendAuthSessionRequest) ProtoMessage() {} + +func (x *RequestExtendAuthSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[86] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestExtendAuthSessionRequest.ProtoReflect.Descriptor instead. +func (*RequestExtendAuthSessionRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{86} +} + +func (x *RequestExtendAuthSessionRequest) GetHint() string { + if x != nil && x.Hint != nil { + return *x.Hint + } + return "" +} + +// RequestExtendAuthSessionResponse carries the verification URI the UI +// should open in a browser. The daemon retains the flow state and resolves +// it via WaitExtendAuthSession. +type RequestExtendAuthSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // verification URI for the user to open in the browser + VerificationURI string `protobuf:"bytes,1,opt,name=verificationURI,proto3" json:"verificationURI,omitempty"` + // complete verification URI (with embedded user code) + VerificationURIComplete string `protobuf:"bytes,2,opt,name=verificationURIComplete,proto3" json:"verificationURIComplete,omitempty"` + // user code to enter on verification URI (for device-code flows) + UserCode string `protobuf:"bytes,3,opt,name=userCode,proto3" json:"userCode,omitempty"` + // device code for matching the WaitExtendAuthSession call to this flow + DeviceCode string `protobuf:"bytes,4,opt,name=deviceCode,proto3" json:"deviceCode,omitempty"` + // expiration time in seconds for the device code / PKCE flow + ExpiresIn int64 `protobuf:"varint,5,opt,name=expiresIn,proto3" json:"expiresIn,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestExtendAuthSessionResponse) Reset() { + *x = RequestExtendAuthSessionResponse{} + mi := &file_daemon_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestExtendAuthSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestExtendAuthSessionResponse) ProtoMessage() {} + +func (x *RequestExtendAuthSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[87] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestExtendAuthSessionResponse.ProtoReflect.Descriptor instead. +func (*RequestExtendAuthSessionResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{87} +} + +func (x *RequestExtendAuthSessionResponse) GetVerificationURI() string { + if x != nil { + return x.VerificationURI + } + return "" +} + +func (x *RequestExtendAuthSessionResponse) GetVerificationURIComplete() string { + if x != nil { + return x.VerificationURIComplete + } + return "" +} + +func (x *RequestExtendAuthSessionResponse) GetUserCode() string { + if x != nil { + return x.UserCode + } + return "" +} + +func (x *RequestExtendAuthSessionResponse) GetDeviceCode() string { + if x != nil { + return x.DeviceCode + } + return "" +} + +func (x *RequestExtendAuthSessionResponse) GetExpiresIn() int64 { + if x != nil { + return x.ExpiresIn + } + return 0 +} + +// WaitExtendAuthSessionRequest is sent by the UI after it opens the +// verification URI. The daemon blocks on this call until the user +// completes (or aborts) the SSO step. +type WaitExtendAuthSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // device code returned by RequestExtendAuthSession + DeviceCode string `protobuf:"bytes,1,opt,name=deviceCode,proto3" json:"deviceCode,omitempty"` + // user code for verification + UserCode string `protobuf:"bytes,2,opt,name=userCode,proto3" json:"userCode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WaitExtendAuthSessionRequest) Reset() { + *x = WaitExtendAuthSessionRequest{} + mi := &file_daemon_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WaitExtendAuthSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WaitExtendAuthSessionRequest) ProtoMessage() {} + +func (x *WaitExtendAuthSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[88] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WaitExtendAuthSessionRequest.ProtoReflect.Descriptor instead. +func (*WaitExtendAuthSessionRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{88} +} + +func (x *WaitExtendAuthSessionRequest) GetDeviceCode() string { + if x != nil { + return x.DeviceCode + } + return "" +} + +func (x *WaitExtendAuthSessionRequest) GetUserCode() string { + if x != nil { + return x.UserCode + } + return "" +} + +// WaitExtendAuthSessionResponse carries the refreshed deadline returned +// by the management server. Unset when the management server reports the +// peer is not eligible for session extension. +type WaitExtendAuthSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionExpiresAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=sessionExpiresAt,proto3" json:"sessionExpiresAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WaitExtendAuthSessionResponse) Reset() { + *x = WaitExtendAuthSessionResponse{} + mi := &file_daemon_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WaitExtendAuthSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WaitExtendAuthSessionResponse) ProtoMessage() {} + +func (x *WaitExtendAuthSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[89] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WaitExtendAuthSessionResponse.ProtoReflect.Descriptor instead. +func (*WaitExtendAuthSessionResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{89} +} + +func (x *WaitExtendAuthSessionResponse) GetSessionExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.SessionExpiresAt + } + return nil +} + +// DismissSessionWarningRequest is sent by the UI when the user clicks +// "Dismiss" on the T-WarningLead notification. +type DismissSessionWarningRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DismissSessionWarningRequest) Reset() { + *x = DismissSessionWarningRequest{} + mi := &file_daemon_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DismissSessionWarningRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DismissSessionWarningRequest) ProtoMessage() {} + +func (x *DismissSessionWarningRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[90] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DismissSessionWarningRequest.ProtoReflect.Descriptor instead. +func (*DismissSessionWarningRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{90} +} + +// DismissSessionWarningResponse acknowledges the dismissal. Carries no +// payload — the daemon's only obligation is to silence the upcoming +// T-FinalWarningLead fallback for the current deadline. +type DismissSessionWarningResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DismissSessionWarningResponse) Reset() { + *x = DismissSessionWarningResponse{} + mi := &file_daemon_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DismissSessionWarningResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DismissSessionWarningResponse) ProtoMessage() {} + +func (x *DismissSessionWarningResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[91] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DismissSessionWarningResponse.ProtoReflect.Descriptor instead. +func (*DismissSessionWarningResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{91} +} + // StartCPUProfileRequest for starting CPU profiling type StartCPUProfileRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -5674,7 +6187,7 @@ type StartCPUProfileRequest struct { func (x *StartCPUProfileRequest) Reset() { *x = StartCPUProfileRequest{} - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5686,7 +6199,7 @@ func (x *StartCPUProfileRequest) String() string { func (*StartCPUProfileRequest) ProtoMessage() {} func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5699,7 +6212,7 @@ func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCPUProfileRequest.ProtoReflect.Descriptor instead. func (*StartCPUProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{82} + return file_daemon_proto_rawDescGZIP(), []int{92} } // StartCPUProfileResponse confirms CPU profiling has started @@ -5711,7 +6224,7 @@ type StartCPUProfileResponse struct { func (x *StartCPUProfileResponse) Reset() { *x = StartCPUProfileResponse{} - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5723,7 +6236,7 @@ func (x *StartCPUProfileResponse) String() string { func (*StartCPUProfileResponse) ProtoMessage() {} func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5736,7 +6249,7 @@ func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCPUProfileResponse.ProtoReflect.Descriptor instead. func (*StartCPUProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{83} + return file_daemon_proto_rawDescGZIP(), []int{93} } // StopCPUProfileRequest for stopping CPU profiling @@ -5748,7 +6261,7 @@ type StopCPUProfileRequest struct { func (x *StopCPUProfileRequest) Reset() { *x = StopCPUProfileRequest{} - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5760,7 +6273,7 @@ func (x *StopCPUProfileRequest) String() string { func (*StopCPUProfileRequest) ProtoMessage() {} func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5773,7 +6286,7 @@ func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopCPUProfileRequest.ProtoReflect.Descriptor instead. func (*StopCPUProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{84} + return file_daemon_proto_rawDescGZIP(), []int{94} } // StopCPUProfileResponse confirms CPU profiling has stopped @@ -5785,7 +6298,7 @@ type StopCPUProfileResponse struct { func (x *StopCPUProfileResponse) Reset() { *x = StopCPUProfileResponse{} - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5797,7 +6310,7 @@ func (x *StopCPUProfileResponse) String() string { func (*StopCPUProfileResponse) ProtoMessage() {} func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5810,7 +6323,7 @@ func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopCPUProfileResponse.ProtoReflect.Descriptor instead. func (*StopCPUProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{85} + return file_daemon_proto_rawDescGZIP(), []int{95} } type InstallerResultRequest struct { @@ -5821,7 +6334,7 @@ type InstallerResultRequest struct { func (x *InstallerResultRequest) Reset() { *x = InstallerResultRequest{} - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5833,7 +6346,7 @@ func (x *InstallerResultRequest) String() string { func (*InstallerResultRequest) ProtoMessage() {} func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5846,7 +6359,7 @@ func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallerResultRequest.ProtoReflect.Descriptor instead. func (*InstallerResultRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{86} + return file_daemon_proto_rawDescGZIP(), []int{96} } type InstallerResultResponse struct { @@ -5859,7 +6372,7 @@ type InstallerResultResponse struct { func (x *InstallerResultResponse) Reset() { *x = InstallerResultResponse{} - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5871,7 +6384,7 @@ func (x *InstallerResultResponse) String() string { func (*InstallerResultResponse) ProtoMessage() {} func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5884,7 +6397,7 @@ func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallerResultResponse.ProtoReflect.Descriptor instead. func (*InstallerResultResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{87} + return file_daemon_proto_rawDescGZIP(), []int{97} } func (x *InstallerResultResponse) GetSuccess() bool { @@ -5917,7 +6430,7 @@ type ExposeServiceRequest struct { func (x *ExposeServiceRequest) Reset() { *x = ExposeServiceRequest{} - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5929,7 +6442,7 @@ func (x *ExposeServiceRequest) String() string { func (*ExposeServiceRequest) ProtoMessage() {} func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5942,7 +6455,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{88} + return file_daemon_proto_rawDescGZIP(), []int{98} } func (x *ExposeServiceRequest) GetPort() uint32 { @@ -6013,7 +6526,7 @@ type ExposeServiceEvent struct { func (x *ExposeServiceEvent) Reset() { *x = ExposeServiceEvent{} - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6025,7 +6538,7 @@ func (x *ExposeServiceEvent) String() string { func (*ExposeServiceEvent) ProtoMessage() {} func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6038,7 +6551,7 @@ func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceEvent.ProtoReflect.Descriptor instead. func (*ExposeServiceEvent) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{89} + return file_daemon_proto_rawDescGZIP(), []int{99} } func (x *ExposeServiceEvent) GetEvent() isExposeServiceEvent_Event { @@ -6079,7 +6592,7 @@ type ExposeServiceReady struct { func (x *ExposeServiceReady) Reset() { *x = ExposeServiceReady{} - mi := &file_daemon_proto_msgTypes[90] + mi := &file_daemon_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6091,7 +6604,7 @@ func (x *ExposeServiceReady) String() string { func (*ExposeServiceReady) ProtoMessage() {} func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[90] + mi := &file_daemon_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6104,7 +6617,7 @@ func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceReady.ProtoReflect.Descriptor instead. func (*ExposeServiceReady) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{90} + return file_daemon_proto_rawDescGZIP(), []int{100} } func (x *ExposeServiceReady) GetServiceName() string { @@ -6149,7 +6662,7 @@ type StartCaptureRequest struct { func (x *StartCaptureRequest) Reset() { *x = StartCaptureRequest{} - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6161,7 +6674,7 @@ func (x *StartCaptureRequest) String() string { func (*StartCaptureRequest) ProtoMessage() {} func (x *StartCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6174,7 +6687,7 @@ func (x *StartCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCaptureRequest.ProtoReflect.Descriptor instead. func (*StartCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{91} + return file_daemon_proto_rawDescGZIP(), []int{101} } func (x *StartCaptureRequest) GetTextOutput() bool { @@ -6228,7 +6741,7 @@ type CapturePacket struct { func (x *CapturePacket) Reset() { *x = CapturePacket{} - mi := &file_daemon_proto_msgTypes[92] + mi := &file_daemon_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6240,7 +6753,7 @@ func (x *CapturePacket) String() string { func (*CapturePacket) ProtoMessage() {} func (x *CapturePacket) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[92] + mi := &file_daemon_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6253,7 +6766,7 @@ func (x *CapturePacket) ProtoReflect() protoreflect.Message { // Deprecated: Use CapturePacket.ProtoReflect.Descriptor instead. func (*CapturePacket) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{92} + return file_daemon_proto_rawDescGZIP(), []int{102} } func (x *CapturePacket) GetData() []byte { @@ -6274,7 +6787,7 @@ type StartBundleCaptureRequest struct { func (x *StartBundleCaptureRequest) Reset() { *x = StartBundleCaptureRequest{} - mi := &file_daemon_proto_msgTypes[93] + mi := &file_daemon_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6286,7 +6799,7 @@ func (x *StartBundleCaptureRequest) String() string { func (*StartBundleCaptureRequest) ProtoMessage() {} func (x *StartBundleCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[93] + mi := &file_daemon_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6299,7 +6812,7 @@ func (x *StartBundleCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartBundleCaptureRequest.ProtoReflect.Descriptor instead. func (*StartBundleCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{93} + return file_daemon_proto_rawDescGZIP(), []int{103} } func (x *StartBundleCaptureRequest) GetTimeout() *durationpb.Duration { @@ -6317,7 +6830,7 @@ type StartBundleCaptureResponse struct { func (x *StartBundleCaptureResponse) Reset() { *x = StartBundleCaptureResponse{} - mi := &file_daemon_proto_msgTypes[94] + mi := &file_daemon_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6329,7 +6842,7 @@ func (x *StartBundleCaptureResponse) String() string { func (*StartBundleCaptureResponse) ProtoMessage() {} func (x *StartBundleCaptureResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[94] + mi := &file_daemon_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6342,7 +6855,7 @@ func (x *StartBundleCaptureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartBundleCaptureResponse.ProtoReflect.Descriptor instead. func (*StartBundleCaptureResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{94} + return file_daemon_proto_rawDescGZIP(), []int{104} } type StopBundleCaptureRequest struct { @@ -6353,7 +6866,7 @@ type StopBundleCaptureRequest struct { func (x *StopBundleCaptureRequest) Reset() { *x = StopBundleCaptureRequest{} - mi := &file_daemon_proto_msgTypes[95] + mi := &file_daemon_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6365,7 +6878,7 @@ func (x *StopBundleCaptureRequest) String() string { func (*StopBundleCaptureRequest) ProtoMessage() {} func (x *StopBundleCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[95] + mi := &file_daemon_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6378,7 +6891,7 @@ func (x *StopBundleCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopBundleCaptureRequest.ProtoReflect.Descriptor instead. func (*StopBundleCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{95} + return file_daemon_proto_rawDescGZIP(), []int{105} } type StopBundleCaptureResponse struct { @@ -6389,7 +6902,7 @@ type StopBundleCaptureResponse struct { func (x *StopBundleCaptureResponse) Reset() { *x = StopBundleCaptureResponse{} - mi := &file_daemon_proto_msgTypes[96] + mi := &file_daemon_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6401,7 +6914,7 @@ func (x *StopBundleCaptureResponse) String() string { func (*StopBundleCaptureResponse) ProtoMessage() {} func (x *StopBundleCaptureResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[96] + mi := &file_daemon_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6414,7 +6927,7 @@ func (x *StopBundleCaptureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopBundleCaptureResponse.ProtoReflect.Descriptor instead. func (*StopBundleCaptureResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{96} + return file_daemon_proto_rawDescGZIP(), []int{106} } type PortInfo_Range struct { @@ -6427,7 +6940,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[98] + mi := &file_daemon_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6439,7 +6952,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[98] + mi := &file_daemon_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6557,10 +7070,11 @@ const file_daemon_proto_rawDesc = "" + "\buserCode\x18\x01 \x01(\tR\buserCode\x12\x1a\n" + "\bhostname\x18\x02 \x01(\tR\bhostname\",\n" + "\x14WaitSSOLoginResponse\x12\x14\n" + - "\x05email\x18\x01 \x01(\tR\x05email\"v\n" + + "\x05email\x18\x01 \x01(\tR\x05email\"\x8c\x01\n" + "\tUpRequest\x12%\n" + "\vprofileName\x18\x01 \x01(\tH\x00R\vprofileName\x88\x01\x01\x12\x1f\n" + - "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01\x12\x14\n" + + "\x05async\x18\x04 \x01(\bR\x05asyncB\x0e\n" + "\f_profileNameB\v\n" + "\t_usernameJ\x04\b\x03\x10\x04\"\f\n" + "\n" + @@ -6569,13 +7083,14 @@ const file_daemon_proto_rawDesc = "" + "\x11getFullPeerStatus\x18\x01 \x01(\bR\x11getFullPeerStatus\x12(\n" + "\x0fshouldRunProbes\x18\x02 \x01(\bR\x0fshouldRunProbes\x12'\n" + "\fwaitForReady\x18\x03 \x01(\bH\x00R\fwaitForReady\x88\x01\x01B\x0f\n" + - "\r_waitForReady\"\x82\x01\n" + + "\r_waitForReady\"\xca\x01\n" + "\x0eStatusResponse\x12\x16\n" + "\x06status\x18\x01 \x01(\tR\x06status\x122\n" + "\n" + "fullStatus\x18\x02 \x01(\v2\x12.daemon.FullStatusR\n" + "fullStatus\x12$\n" + - "\rdaemonVersion\x18\x03 \x01(\tR\rdaemonVersion\"\r\n" + + "\rdaemonVersion\x18\x03 \x01(\tR\rdaemonVersion\x12F\n" + + "\x10sessionExpiresAt\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x10sessionExpiresAt\"\r\n" + "\vDownRequest\"\x0e\n" + "\fDownResponse\"P\n" + "\x10GetConfigRequest\x12 \n" + @@ -6676,7 +7191,7 @@ const file_daemon_proto_rawDesc = "" + "\fportForwards\x18\x05 \x03(\tR\fportForwards\"^\n" + "\x0eSSHServerState\x12\x18\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x122\n" + - "\bsessions\x18\x02 \x03(\v2\x16.daemon.SSHSessionInfoR\bsessions\"\xaf\x04\n" + + "\bsessions\x18\x02 \x03(\v2\x16.daemon.SSHSessionInfoR\bsessions\"\xdb\x04\n" + "\n" + "FullStatus\x12A\n" + "\x0fmanagementState\x18\x01 \x01(\v2\x17.daemon.ManagementStateR\x0fmanagementState\x125\n" + @@ -6690,7 +7205,8 @@ const file_daemon_proto_rawDesc = "" + "\x06events\x18\a \x03(\v2\x13.daemon.SystemEventR\x06events\x124\n" + "\x15lazyConnectionEnabled\x18\t \x01(\bR\x15lazyConnectionEnabled\x12>\n" + "\x0esshServerState\x18\n" + - " \x01(\v2\x16.daemon.SSHServerStateR\x0esshServerState\"\x15\n" + + " \x01(\v2\x16.daemon.SSHServerStateR\x0esshServerState\x12*\n" + + "\x10networksRevision\x18\v \x01(\x04R\x10networksRevision\"\x15\n" + "\x13ListNetworksRequest\"?\n" + "\x14ListNetworksResponse\x12'\n" + "\x06routes\x18\x01 \x03(\v2\x0f.daemon.NetworkR\x06routes\"a\n" + @@ -6746,7 +7262,10 @@ const file_daemon_proto_rawDesc = "" + "\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\"<\n" + "\x12SetLogLevelRequest\x12&\n" + "\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\"\x15\n" + - "\x13SetLogLevelResponse\"\x1b\n" + + "\x13SetLogLevelResponse\"*\n" + + "\x14RegisterUILogRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\"\x17\n" + + "\x15RegisterUILogResponse\"\x1b\n" + "\x05State\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"\x13\n" + "\x11ListStatesRequest\";\n" + @@ -6935,12 +7454,16 @@ const file_daemon_proto_rawDesc = "" + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + "\f_profileNameB\v\n" + "\t_username\"\x10\n" + - "\x0eLogoutResponse\"\x14\n" + - "\x12GetFeaturesRequest\"\xa3\x01\n" + + "\x0eLogoutResponse\"\x15\n" + + "\x13WailsUIReadyRequest\"\x16\n" + + "\x14WailsUIReadyResponse\"\x14\n" + + "\x12GetFeaturesRequest\"\xf6\x01\n" + "\x13GetFeaturesResponse\x12)\n" + "\x10disable_profiles\x18\x01 \x01(\bR\x0fdisableProfiles\x126\n" + "\x17disable_update_settings\x18\x02 \x01(\bR\x15disableUpdateSettings\x12)\n" + - "\x10disable_networks\x18\x03 \x01(\bR\x0fdisableNetworks\"3\n" + + "\x10disable_networks\x18\x03 \x01(\bR\x0fdisableNetworks\x127\n" + + "\x15disable_advanced_view\x18\x04 \x01(\bH\x00R\x13disableAdvancedView\x88\x01\x01B\x18\n" + + "\x16_disable_advanced_view\"3\n" + "\x19MDMManagedFieldsViolation\x12\x16\n" + "\x06fields\x18\x01 \x03(\tR\x06fields\"\x16\n" + "\x14TriggerUpdateRequest\"M\n" + @@ -6977,7 +7500,27 @@ const file_daemon_proto_rawDesc = "" + "\x14WaitJWTTokenResponse\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x12\x1c\n" + "\ttokenType\x18\x02 \x01(\tR\ttokenType\x12\x1c\n" + - "\texpiresIn\x18\x03 \x01(\x03R\texpiresIn\"\x18\n" + + "\texpiresIn\x18\x03 \x01(\x03R\texpiresIn\"C\n" + + "\x1fRequestExtendAuthSessionRequest\x12\x17\n" + + "\x04hint\x18\x01 \x01(\tH\x00R\x04hint\x88\x01\x01B\a\n" + + "\x05_hint\"\xe0\x01\n" + + " RequestExtendAuthSessionResponse\x12(\n" + + "\x0fverificationURI\x18\x01 \x01(\tR\x0fverificationURI\x128\n" + + "\x17verificationURIComplete\x18\x02 \x01(\tR\x17verificationURIComplete\x12\x1a\n" + + "\buserCode\x18\x03 \x01(\tR\buserCode\x12\x1e\n" + + "\n" + + "deviceCode\x18\x04 \x01(\tR\n" + + "deviceCode\x12\x1c\n" + + "\texpiresIn\x18\x05 \x01(\x03R\texpiresIn\"Z\n" + + "\x1cWaitExtendAuthSessionRequest\x12\x1e\n" + + "\n" + + "deviceCode\x18\x01 \x01(\tR\n" + + "deviceCode\x12\x1a\n" + + "\buserCode\x18\x02 \x01(\tR\buserCode\"g\n" + + "\x1dWaitExtendAuthSessionResponse\x12F\n" + + "\x10sessionExpiresAt\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x10sessionExpiresAt\"\x1e\n" + + "\x1cDismissSessionWarningRequest\"\x1f\n" + + "\x1dDismissSessionWarningResponse\"\x18\n" + "\x16StartCPUProfileRequest\"\x19\n" + "\x17StartCPUProfileResponse\"\x17\n" + "\x15StopCPUProfileRequest\"\x18\n" + @@ -7040,12 +7583,13 @@ const file_daemon_proto_rawDesc = "" + "\n" + "EXPOSE_UDP\x10\x03\x12\x0e\n" + "\n" + - "EXPOSE_TLS\x10\x042\xff\x17\n" + + "EXPOSE_TLS\x10\x042\xa3\x1c\n" + "\rDaemonService\x126\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + "\x02Up\x12\x11.daemon.UpRequest\x1a\x12.daemon.UpResponse\"\x00\x129\n" + - "\x06Status\x12\x15.daemon.StatusRequest\x1a\x16.daemon.StatusResponse\"\x00\x123\n" + + "\x06Status\x12\x15.daemon.StatusRequest\x1a\x16.daemon.StatusResponse\"\x00\x12D\n" + + "\x0fSubscribeStatus\x12\x15.daemon.StatusRequest\x1a\x16.daemon.StatusResponse\"\x000\x01\x123\n" + "\x04Down\x12\x13.daemon.DownRequest\x1a\x14.daemon.DownResponse\"\x00\x12B\n" + "\tGetConfig\x12\x18.daemon.GetConfigRequest\x1a\x19.daemon.GetConfigResponse\"\x00\x12K\n" + "\fListNetworks\x12\x1b.daemon.ListNetworksRequest\x1a\x1c.daemon.ListNetworksResponse\"\x00\x12Q\n" + @@ -7067,6 +7611,7 @@ const file_daemon_proto_rawDesc = "" + "\x11StopBundleCapture\x12 .daemon.StopBundleCaptureRequest\x1a!.daemon.StopBundleCaptureResponse\"\x00\x12D\n" + "\x0fSubscribeEvents\x12\x18.daemon.SubscribeRequest\x1a\x13.daemon.SystemEvent\"\x000\x01\x12B\n" + "\tGetEvents\x12\x18.daemon.GetEventsRequest\x1a\x19.daemon.GetEventsResponse\"\x00\x12N\n" + + "\rRegisterUILog\x12\x1c.daemon.RegisterUILogRequest\x1a\x1d.daemon.RegisterUILogResponse\"\x00\x12N\n" + "\rSwitchProfile\x12\x1c.daemon.SwitchProfileRequest\x1a\x1d.daemon.SwitchProfileResponse\"\x00\x12B\n" + "\tSetConfig\x12\x18.daemon.SetConfigRequest\x1a\x19.daemon.SetConfigResponse\"\x00\x12E\n" + "\n" + @@ -7080,11 +7625,15 @@ const file_daemon_proto_rawDesc = "" + "\rTriggerUpdate\x12\x1c.daemon.TriggerUpdateRequest\x1a\x1d.daemon.TriggerUpdateResponse\"\x00\x12Z\n" + "\x11GetPeerSSHHostKey\x12 .daemon.GetPeerSSHHostKeyRequest\x1a!.daemon.GetPeerSSHHostKeyResponse\"\x00\x12Q\n" + "\x0eRequestJWTAuth\x12\x1d.daemon.RequestJWTAuthRequest\x1a\x1e.daemon.RequestJWTAuthResponse\"\x00\x12K\n" + - "\fWaitJWTToken\x12\x1b.daemon.WaitJWTTokenRequest\x1a\x1c.daemon.WaitJWTTokenResponse\"\x00\x12T\n" + + "\fWaitJWTToken\x12\x1b.daemon.WaitJWTTokenRequest\x1a\x1c.daemon.WaitJWTTokenResponse\"\x00\x12o\n" + + "\x18RequestExtendAuthSession\x12'.daemon.RequestExtendAuthSessionRequest\x1a(.daemon.RequestExtendAuthSessionResponse\"\x00\x12f\n" + + "\x15WaitExtendAuthSession\x12$.daemon.WaitExtendAuthSessionRequest\x1a%.daemon.WaitExtendAuthSessionResponse\"\x00\x12f\n" + + "\x15DismissSessionWarning\x12$.daemon.DismissSessionWarningRequest\x1a%.daemon.DismissSessionWarningResponse\"\x00\x12T\n" + "\x0fStartCPUProfile\x12\x1e.daemon.StartCPUProfileRequest\x1a\x1f.daemon.StartCPUProfileResponse\"\x00\x12Q\n" + "\x0eStopCPUProfile\x12\x1d.daemon.StopCPUProfileRequest\x1a\x1e.daemon.StopCPUProfileResponse\"\x00\x12W\n" + "\x12GetInstallerResult\x12\x1e.daemon.InstallerResultRequest\x1a\x1f.daemon.InstallerResultResponse\"\x00\x12M\n" + - "\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01B\bZ\x06/protob\x06proto3" + "\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01\x12K\n" + + "\fWailsUIReady\x12\x1b.daemon.WailsUIReadyRequest\x1a\x1c.daemon.WailsUIReadyResponse\"\x00B\bZ\x06/protob\x06proto3" var ( file_daemon_proto_rawDescOnce sync.Once @@ -7099,7 +7648,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 100) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 110) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ExposeProtocol)(0), // 1: daemon.ExposeProtocol @@ -7142,195 +7691,219 @@ var file_daemon_proto_goTypes = []any{ (*GetLogLevelResponse)(nil), // 38: daemon.GetLogLevelResponse (*SetLogLevelRequest)(nil), // 39: daemon.SetLogLevelRequest (*SetLogLevelResponse)(nil), // 40: daemon.SetLogLevelResponse - (*State)(nil), // 41: daemon.State - (*ListStatesRequest)(nil), // 42: daemon.ListStatesRequest - (*ListStatesResponse)(nil), // 43: daemon.ListStatesResponse - (*CleanStateRequest)(nil), // 44: daemon.CleanStateRequest - (*CleanStateResponse)(nil), // 45: daemon.CleanStateResponse - (*DeleteStateRequest)(nil), // 46: daemon.DeleteStateRequest - (*DeleteStateResponse)(nil), // 47: daemon.DeleteStateResponse - (*SetSyncResponsePersistenceRequest)(nil), // 48: daemon.SetSyncResponsePersistenceRequest - (*SetSyncResponsePersistenceResponse)(nil), // 49: daemon.SetSyncResponsePersistenceResponse - (*TCPFlags)(nil), // 50: daemon.TCPFlags - (*TracePacketRequest)(nil), // 51: daemon.TracePacketRequest - (*TraceStage)(nil), // 52: daemon.TraceStage - (*TracePacketResponse)(nil), // 53: daemon.TracePacketResponse - (*SubscribeRequest)(nil), // 54: daemon.SubscribeRequest - (*SystemEvent)(nil), // 55: daemon.SystemEvent - (*GetEventsRequest)(nil), // 56: daemon.GetEventsRequest - (*GetEventsResponse)(nil), // 57: daemon.GetEventsResponse - (*SwitchProfileRequest)(nil), // 58: daemon.SwitchProfileRequest - (*SwitchProfileResponse)(nil), // 59: daemon.SwitchProfileResponse - (*SetConfigRequest)(nil), // 60: daemon.SetConfigRequest - (*SetConfigResponse)(nil), // 61: daemon.SetConfigResponse - (*AddProfileRequest)(nil), // 62: daemon.AddProfileRequest - (*AddProfileResponse)(nil), // 63: daemon.AddProfileResponse - (*RenameProfileRequest)(nil), // 64: daemon.RenameProfileRequest - (*RenameProfileResponse)(nil), // 65: daemon.RenameProfileResponse - (*RemoveProfileRequest)(nil), // 66: daemon.RemoveProfileRequest - (*RemoveProfileResponse)(nil), // 67: daemon.RemoveProfileResponse - (*ListProfilesRequest)(nil), // 68: daemon.ListProfilesRequest - (*ListProfilesResponse)(nil), // 69: daemon.ListProfilesResponse - (*Profile)(nil), // 70: daemon.Profile - (*GetActiveProfileRequest)(nil), // 71: daemon.GetActiveProfileRequest - (*GetActiveProfileResponse)(nil), // 72: daemon.GetActiveProfileResponse - (*LogoutRequest)(nil), // 73: daemon.LogoutRequest - (*LogoutResponse)(nil), // 74: daemon.LogoutResponse - (*GetFeaturesRequest)(nil), // 75: daemon.GetFeaturesRequest - (*GetFeaturesResponse)(nil), // 76: daemon.GetFeaturesResponse - (*MDMManagedFieldsViolation)(nil), // 77: daemon.MDMManagedFieldsViolation - (*TriggerUpdateRequest)(nil), // 78: daemon.TriggerUpdateRequest - (*TriggerUpdateResponse)(nil), // 79: daemon.TriggerUpdateResponse - (*GetPeerSSHHostKeyRequest)(nil), // 80: daemon.GetPeerSSHHostKeyRequest - (*GetPeerSSHHostKeyResponse)(nil), // 81: daemon.GetPeerSSHHostKeyResponse - (*RequestJWTAuthRequest)(nil), // 82: daemon.RequestJWTAuthRequest - (*RequestJWTAuthResponse)(nil), // 83: daemon.RequestJWTAuthResponse - (*WaitJWTTokenRequest)(nil), // 84: daemon.WaitJWTTokenRequest - (*WaitJWTTokenResponse)(nil), // 85: daemon.WaitJWTTokenResponse - (*StartCPUProfileRequest)(nil), // 86: daemon.StartCPUProfileRequest - (*StartCPUProfileResponse)(nil), // 87: daemon.StartCPUProfileResponse - (*StopCPUProfileRequest)(nil), // 88: daemon.StopCPUProfileRequest - (*StopCPUProfileResponse)(nil), // 89: daemon.StopCPUProfileResponse - (*InstallerResultRequest)(nil), // 90: daemon.InstallerResultRequest - (*InstallerResultResponse)(nil), // 91: daemon.InstallerResultResponse - (*ExposeServiceRequest)(nil), // 92: daemon.ExposeServiceRequest - (*ExposeServiceEvent)(nil), // 93: daemon.ExposeServiceEvent - (*ExposeServiceReady)(nil), // 94: daemon.ExposeServiceReady - (*StartCaptureRequest)(nil), // 95: daemon.StartCaptureRequest - (*CapturePacket)(nil), // 96: daemon.CapturePacket - (*StartBundleCaptureRequest)(nil), // 97: daemon.StartBundleCaptureRequest - (*StartBundleCaptureResponse)(nil), // 98: daemon.StartBundleCaptureResponse - (*StopBundleCaptureRequest)(nil), // 99: daemon.StopBundleCaptureRequest - (*StopBundleCaptureResponse)(nil), // 100: daemon.StopBundleCaptureResponse - nil, // 101: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 102: daemon.PortInfo.Range - nil, // 103: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 104: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 105: google.protobuf.Timestamp + (*RegisterUILogRequest)(nil), // 41: daemon.RegisterUILogRequest + (*RegisterUILogResponse)(nil), // 42: daemon.RegisterUILogResponse + (*State)(nil), // 43: daemon.State + (*ListStatesRequest)(nil), // 44: daemon.ListStatesRequest + (*ListStatesResponse)(nil), // 45: daemon.ListStatesResponse + (*CleanStateRequest)(nil), // 46: daemon.CleanStateRequest + (*CleanStateResponse)(nil), // 47: daemon.CleanStateResponse + (*DeleteStateRequest)(nil), // 48: daemon.DeleteStateRequest + (*DeleteStateResponse)(nil), // 49: daemon.DeleteStateResponse + (*SetSyncResponsePersistenceRequest)(nil), // 50: daemon.SetSyncResponsePersistenceRequest + (*SetSyncResponsePersistenceResponse)(nil), // 51: daemon.SetSyncResponsePersistenceResponse + (*TCPFlags)(nil), // 52: daemon.TCPFlags + (*TracePacketRequest)(nil), // 53: daemon.TracePacketRequest + (*TraceStage)(nil), // 54: daemon.TraceStage + (*TracePacketResponse)(nil), // 55: daemon.TracePacketResponse + (*SubscribeRequest)(nil), // 56: daemon.SubscribeRequest + (*SystemEvent)(nil), // 57: daemon.SystemEvent + (*GetEventsRequest)(nil), // 58: daemon.GetEventsRequest + (*GetEventsResponse)(nil), // 59: daemon.GetEventsResponse + (*SwitchProfileRequest)(nil), // 60: daemon.SwitchProfileRequest + (*SwitchProfileResponse)(nil), // 61: daemon.SwitchProfileResponse + (*SetConfigRequest)(nil), // 62: daemon.SetConfigRequest + (*SetConfigResponse)(nil), // 63: daemon.SetConfigResponse + (*AddProfileRequest)(nil), // 64: daemon.AddProfileRequest + (*AddProfileResponse)(nil), // 65: daemon.AddProfileResponse + (*RenameProfileRequest)(nil), // 66: daemon.RenameProfileRequest + (*RenameProfileResponse)(nil), // 67: daemon.RenameProfileResponse + (*RemoveProfileRequest)(nil), // 68: daemon.RemoveProfileRequest + (*RemoveProfileResponse)(nil), // 69: daemon.RemoveProfileResponse + (*ListProfilesRequest)(nil), // 70: daemon.ListProfilesRequest + (*ListProfilesResponse)(nil), // 71: daemon.ListProfilesResponse + (*Profile)(nil), // 72: daemon.Profile + (*GetActiveProfileRequest)(nil), // 73: daemon.GetActiveProfileRequest + (*GetActiveProfileResponse)(nil), // 74: daemon.GetActiveProfileResponse + (*LogoutRequest)(nil), // 75: daemon.LogoutRequest + (*LogoutResponse)(nil), // 76: daemon.LogoutResponse + (*WailsUIReadyRequest)(nil), // 77: daemon.WailsUIReadyRequest + (*WailsUIReadyResponse)(nil), // 78: daemon.WailsUIReadyResponse + (*GetFeaturesRequest)(nil), // 79: daemon.GetFeaturesRequest + (*GetFeaturesResponse)(nil), // 80: daemon.GetFeaturesResponse + (*MDMManagedFieldsViolation)(nil), // 81: daemon.MDMManagedFieldsViolation + (*TriggerUpdateRequest)(nil), // 82: daemon.TriggerUpdateRequest + (*TriggerUpdateResponse)(nil), // 83: daemon.TriggerUpdateResponse + (*GetPeerSSHHostKeyRequest)(nil), // 84: daemon.GetPeerSSHHostKeyRequest + (*GetPeerSSHHostKeyResponse)(nil), // 85: daemon.GetPeerSSHHostKeyResponse + (*RequestJWTAuthRequest)(nil), // 86: daemon.RequestJWTAuthRequest + (*RequestJWTAuthResponse)(nil), // 87: daemon.RequestJWTAuthResponse + (*WaitJWTTokenRequest)(nil), // 88: daemon.WaitJWTTokenRequest + (*WaitJWTTokenResponse)(nil), // 89: daemon.WaitJWTTokenResponse + (*RequestExtendAuthSessionRequest)(nil), // 90: daemon.RequestExtendAuthSessionRequest + (*RequestExtendAuthSessionResponse)(nil), // 91: daemon.RequestExtendAuthSessionResponse + (*WaitExtendAuthSessionRequest)(nil), // 92: daemon.WaitExtendAuthSessionRequest + (*WaitExtendAuthSessionResponse)(nil), // 93: daemon.WaitExtendAuthSessionResponse + (*DismissSessionWarningRequest)(nil), // 94: daemon.DismissSessionWarningRequest + (*DismissSessionWarningResponse)(nil), // 95: daemon.DismissSessionWarningResponse + (*StartCPUProfileRequest)(nil), // 96: daemon.StartCPUProfileRequest + (*StartCPUProfileResponse)(nil), // 97: daemon.StartCPUProfileResponse + (*StopCPUProfileRequest)(nil), // 98: daemon.StopCPUProfileRequest + (*StopCPUProfileResponse)(nil), // 99: daemon.StopCPUProfileResponse + (*InstallerResultRequest)(nil), // 100: daemon.InstallerResultRequest + (*InstallerResultResponse)(nil), // 101: daemon.InstallerResultResponse + (*ExposeServiceRequest)(nil), // 102: daemon.ExposeServiceRequest + (*ExposeServiceEvent)(nil), // 103: daemon.ExposeServiceEvent + (*ExposeServiceReady)(nil), // 104: daemon.ExposeServiceReady + (*StartCaptureRequest)(nil), // 105: daemon.StartCaptureRequest + (*CapturePacket)(nil), // 106: daemon.CapturePacket + (*StartBundleCaptureRequest)(nil), // 107: daemon.StartBundleCaptureRequest + (*StartBundleCaptureResponse)(nil), // 108: daemon.StartBundleCaptureResponse + (*StopBundleCaptureRequest)(nil), // 109: daemon.StopBundleCaptureRequest + (*StopBundleCaptureResponse)(nil), // 110: daemon.StopBundleCaptureResponse + nil, // 111: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 112: daemon.PortInfo.Range + nil, // 113: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 114: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 115: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 104, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 114, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 25, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 105, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 105, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 104, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration - 23, // 5: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo - 20, // 6: daemon.FullStatus.managementState:type_name -> daemon.ManagementState - 19, // 7: daemon.FullStatus.signalState:type_name -> daemon.SignalState - 18, // 8: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState - 17, // 9: daemon.FullStatus.peers:type_name -> daemon.PeerState - 21, // 10: daemon.FullStatus.relays:type_name -> daemon.RelayState - 22, // 11: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState - 55, // 12: daemon.FullStatus.events:type_name -> daemon.SystemEvent - 24, // 13: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState - 31, // 14: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 101, // 15: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 102, // 16: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range - 32, // 17: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo - 32, // 18: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo - 33, // 19: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule - 0, // 20: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel - 0, // 21: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel - 41, // 22: daemon.ListStatesResponse.states:type_name -> daemon.State - 50, // 23: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags - 52, // 24: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage - 2, // 25: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity - 3, // 26: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 105, // 27: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 103, // 28: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry - 55, // 29: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 104, // 30: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 70, // 31: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile - 1, // 32: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol - 94, // 33: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady - 104, // 34: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration - 104, // 35: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration - 30, // 36: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList - 5, // 37: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 7, // 38: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 9, // 39: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 11, // 40: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 13, // 41: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 15, // 42: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 26, // 43: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest - 28, // 44: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest - 28, // 45: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest - 4, // 46: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest - 35, // 47: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 37, // 48: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 39, // 49: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 42, // 50: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest - 44, // 51: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest - 46, // 52: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest - 48, // 53: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest - 51, // 54: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest - 95, // 55: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest - 97, // 56: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest - 99, // 57: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest - 54, // 58: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest - 56, // 59: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest - 58, // 60: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest - 60, // 61: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest - 62, // 62: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest - 64, // 63: daemon.DaemonService.RenameProfile:input_type -> daemon.RenameProfileRequest - 66, // 64: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest - 68, // 65: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest - 71, // 66: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest - 73, // 67: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest - 75, // 68: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest - 78, // 69: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest - 80, // 70: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest - 82, // 71: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest - 84, // 72: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest - 86, // 73: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest - 88, // 74: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest - 90, // 75: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest - 92, // 76: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest - 6, // 77: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 8, // 78: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 10, // 79: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 12, // 80: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 14, // 81: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 16, // 82: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 27, // 83: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 29, // 84: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 29, // 85: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 34, // 86: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 36, // 87: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 38, // 88: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 40, // 89: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 43, // 90: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 45, // 91: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 47, // 92: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 49, // 93: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse - 53, // 94: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 96, // 95: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket - 98, // 96: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse - 100, // 97: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse - 55, // 98: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 57, // 99: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 59, // 100: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 61, // 101: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 63, // 102: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 65, // 103: daemon.DaemonService.RenameProfile:output_type -> daemon.RenameProfileResponse - 67, // 104: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 69, // 105: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 72, // 106: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 74, // 107: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse - 76, // 108: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse - 79, // 109: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse - 81, // 110: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse - 83, // 111: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse - 85, // 112: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse - 87, // 113: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse - 89, // 114: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse - 91, // 115: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse - 93, // 116: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent - 77, // [77:117] is the sub-list for method output_type - 37, // [37:77] is the sub-list for method input_type - 37, // [37:37] is the sub-list for extension type_name - 37, // [37:37] is the sub-list for extension extendee - 0, // [0:37] is the sub-list for field type_name + 115, // 2: daemon.StatusResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp + 115, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 115, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 114, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 23, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo + 20, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState + 19, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState + 18, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState + 17, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState + 21, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState + 22, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState + 57, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent + 24, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState + 31, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network + 111, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 112, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 32, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo + 32, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo + 33, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule + 0, // 21: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel + 0, // 22: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel + 43, // 23: daemon.ListStatesResponse.states:type_name -> daemon.State + 52, // 24: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags + 54, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage + 2, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity + 3, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category + 115, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 113, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 57, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent + 114, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 72, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile + 115, // 33: daemon.WaitExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp + 1, // 34: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol + 104, // 35: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady + 114, // 36: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration + 114, // 37: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration + 30, // 38: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList + 5, // 39: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 7, // 40: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 9, // 41: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 11, // 42: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 11, // 43: daemon.DaemonService.SubscribeStatus:input_type -> daemon.StatusRequest + 13, // 44: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 15, // 45: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 26, // 46: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest + 28, // 47: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest + 28, // 48: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest + 4, // 49: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest + 35, // 50: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 37, // 51: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 39, // 52: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 44, // 53: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest + 46, // 54: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest + 48, // 55: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest + 50, // 56: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest + 53, // 57: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest + 105, // 58: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest + 107, // 59: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest + 109, // 60: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest + 56, // 61: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest + 58, // 62: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest + 41, // 63: daemon.DaemonService.RegisterUILog:input_type -> daemon.RegisterUILogRequest + 60, // 64: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest + 62, // 65: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest + 64, // 66: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest + 66, // 67: daemon.DaemonService.RenameProfile:input_type -> daemon.RenameProfileRequest + 68, // 68: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest + 70, // 69: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest + 73, // 70: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest + 75, // 71: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest + 79, // 72: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest + 82, // 73: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest + 84, // 74: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest + 86, // 75: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest + 88, // 76: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest + 90, // 77: daemon.DaemonService.RequestExtendAuthSession:input_type -> daemon.RequestExtendAuthSessionRequest + 92, // 78: daemon.DaemonService.WaitExtendAuthSession:input_type -> daemon.WaitExtendAuthSessionRequest + 94, // 79: daemon.DaemonService.DismissSessionWarning:input_type -> daemon.DismissSessionWarningRequest + 96, // 80: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest + 98, // 81: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest + 100, // 82: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest + 102, // 83: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest + 77, // 84: daemon.DaemonService.WailsUIReady:input_type -> daemon.WailsUIReadyRequest + 6, // 85: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 8, // 86: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 10, // 87: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 12, // 88: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 12, // 89: daemon.DaemonService.SubscribeStatus:output_type -> daemon.StatusResponse + 14, // 90: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 16, // 91: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 27, // 92: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 29, // 93: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 29, // 94: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 34, // 95: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 36, // 96: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 38, // 97: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 40, // 98: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 45, // 99: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 47, // 100: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 49, // 101: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 51, // 102: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse + 55, // 103: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 106, // 104: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket + 108, // 105: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse + 110, // 106: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse + 57, // 107: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 59, // 108: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 42, // 109: daemon.DaemonService.RegisterUILog:output_type -> daemon.RegisterUILogResponse + 61, // 110: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 63, // 111: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 65, // 112: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 67, // 113: daemon.DaemonService.RenameProfile:output_type -> daemon.RenameProfileResponse + 69, // 114: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 71, // 115: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 74, // 116: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 76, // 117: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 80, // 118: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse + 83, // 119: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse + 85, // 120: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse + 87, // 121: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse + 89, // 122: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse + 91, // 123: daemon.DaemonService.RequestExtendAuthSession:output_type -> daemon.RequestExtendAuthSessionResponse + 93, // 124: daemon.DaemonService.WaitExtendAuthSession:output_type -> daemon.WaitExtendAuthSessionResponse + 95, // 125: daemon.DaemonService.DismissSessionWarning:output_type -> daemon.DismissSessionWarningResponse + 97, // 126: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse + 99, // 127: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse + 101, // 128: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse + 103, // 129: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent + 78, // 130: daemon.DaemonService.WailsUIReady:output_type -> daemon.WailsUIReadyResponse + 85, // [85:131] is the sub-list for method output_type + 39, // [39:85] is the sub-list for method input_type + 39, // [39:39] is the sub-list for extension type_name + 39, // [39:39] is the sub-list for extension extendee + 0, // [0:39] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -7345,13 +7918,15 @@ func file_daemon_proto_init() { (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } - file_daemon_proto_msgTypes[47].OneofWrappers = []any{} - file_daemon_proto_msgTypes[48].OneofWrappers = []any{} - file_daemon_proto_msgTypes[54].OneofWrappers = []any{} + file_daemon_proto_msgTypes[49].OneofWrappers = []any{} + file_daemon_proto_msgTypes[50].OneofWrappers = []any{} file_daemon_proto_msgTypes[56].OneofWrappers = []any{} - file_daemon_proto_msgTypes[69].OneofWrappers = []any{} - file_daemon_proto_msgTypes[78].OneofWrappers = []any{} - file_daemon_proto_msgTypes[89].OneofWrappers = []any{ + file_daemon_proto_msgTypes[58].OneofWrappers = []any{} + file_daemon_proto_msgTypes[71].OneofWrappers = []any{} + file_daemon_proto_msgTypes[76].OneofWrappers = []any{} + file_daemon_proto_msgTypes[82].OneofWrappers = []any{} + file_daemon_proto_msgTypes[86].OneofWrappers = []any{} + file_daemon_proto_msgTypes[99].OneofWrappers = []any{ (*ExposeServiceEvent_Ready)(nil), } type x struct{} @@ -7360,7 +7935,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), NumEnums: 4, - NumMessages: 100, + NumMessages: 110, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index c1e3fe513..8d5294eb7 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -24,6 +24,12 @@ service DaemonService { // Status of the service. rpc Status(StatusRequest) returns (StatusResponse) {} + // SubscribeStatus pushes a fresh StatusResponse on connection state + // changes (Connected / Disconnected / Connecting / address change / + // peers list change). The first message on the stream is the current + // snapshot, so a freshly-subscribed UI doesn't need to also call Status. + rpc SubscribeStatus(StatusRequest) returns (stream StatusResponse) {} + // Down stops engine work in the daemon. rpc Down(DownRequest) returns (DownResponse) {} @@ -79,6 +85,11 @@ service DaemonService { rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {} + // RegisterUILog records the desktop UI's absolute log path so the daemon's + // debug bundle can collect it (the daemon runs as root and can't resolve the + // user's config dir). + rpc RegisterUILog(RegisterUILogRequest) returns (RegisterUILogResponse) {} + rpc SwitchProfile(SwitchProfileRequest) returns (SwitchProfileResponse) {} rpc SetConfig(SetConfigRequest) returns (SetConfigResponse) {} @@ -111,6 +122,25 @@ service DaemonService { // WaitJWTToken waits for JWT authentication completion rpc WaitJWTToken(WaitJWTTokenRequest) returns (WaitJWTTokenResponse) {} + // RequestExtendAuthSession initiates an SSO session-extension flow. + // The daemon prepares a PKCE/device-code request against the IdP and + // returns the verification URI; the UI is expected to open it. The flow + // state is kept in the daemon until WaitExtendAuthSession completes it. + rpc RequestExtendAuthSession(RequestExtendAuthSessionRequest) returns (RequestExtendAuthSessionResponse) {} + + // WaitExtendAuthSession blocks until the user finishes the SSO step + // started by RequestExtendAuthSession, then forwards the resulting JWT + // to the management server's ExtendAuthSession RPC. Returns the new + // session expiry deadline. The tunnel stays up the entire time. + rpc WaitExtendAuthSession(WaitExtendAuthSessionRequest) returns (WaitExtendAuthSessionResponse) {} + + // DismissSessionWarning records that the user clicked "Dismiss" on the + // T-WarningLead interactive notification, suppressing the auto-opened + // SessionAboutToExpire dialog that would otherwise fire at + // T-FinalWarningLead for the current deadline. Idempotent and best-effort: + // a missed call only means the fallback dialog will still appear. + rpc DismissSessionWarning(DismissSessionWarningRequest) returns (DismissSessionWarningResponse) {} + // StartCPUProfile starts CPU profiling in the daemon rpc StartCPUProfile(StartCPUProfileRequest) returns (StartCPUProfileResponse) {} @@ -121,6 +151,11 @@ service DaemonService { // ExposeService exposes a local port via the NetBird reverse proxy rpc ExposeService(ExposeServiceRequest) returns (stream ExposeServiceEvent) {} + + // WailsUIReady is a no-op probe the Wails UI calls once at startup. The UI + // only cares whether the daemon implements it: an Unimplemented response + // means the daemon predates this UI and is too old to drive it. + rpc WailsUIReady(WailsUIReadyRequest) returns (WailsUIReadyResponse) {} } @@ -229,6 +264,12 @@ message UpRequest { optional string profileName = 1; optional string username = 2; reserved 3; + // async instructs the daemon to start the connection attempt and return + // immediately without waiting for the engine to become ready. Status updates + // are delivered via the SubscribeStatus stream. When false (the default) the + // RPC blocks until the engine is running or gives up, which is the behaviour + // needed by the CLI. + bool async = 4; } message UpResponse {} @@ -246,6 +287,10 @@ message StatusResponse{ FullStatus fullStatus = 2; // NetBird daemon version string daemonVersion = 3; + // Absolute UTC instant at which the peer's SSO session expires. + // Unset when the peer is not SSO-registered or login expiration is disabled. + // The UI derives "warning active" from this value and its own clock. + google.protobuf.Timestamp sessionExpiresAt = 4; } message DownRequest {} @@ -421,6 +466,12 @@ message FullStatus { bool lazyConnectionEnabled = 9; SSHServerState sshServerState = 10; + + // networksRevision bumps whenever the set of routed networks (route and + // exit-node candidates) or their selected state changes. The UI fingerprints + // on it to know when to re-fetch ListNetworks via the push stream, instead + // of polling on every status snapshot. + uint64 networksRevision = 11; } // Networks @@ -518,6 +569,13 @@ message SetLogLevelRequest { message SetLogLevelResponse { } +message RegisterUILogRequest { + string path = 1; +} + +message RegisterUILogResponse { +} + // State represents a daemon state entry message State { string name = 1; @@ -771,12 +829,22 @@ message LogoutRequest { message LogoutResponse {} +message WailsUIReadyRequest {} + +message WailsUIReadyResponse {} + message GetFeaturesRequest{} message GetFeaturesResponse{ bool disable_profiles = 1; bool disable_update_settings = 2; bool disable_networks = 3; + // disableAdvancedView gates the upcoming UI revision's advanced + // section. Tristate: unset = no MDM directive, the UI applies its + // own default; true = MDM enforces disable; false = MDM enforces + // enable. Sourced exclusively from the MDM policy — no CLI / + // config flag backs this value. + optional bool disable_advanced_view = 4; } // MDMManagedFieldsViolation is attached as a gRPC error detail on a @@ -855,6 +923,55 @@ message WaitJWTTokenResponse { int64 expiresIn = 3; } +// RequestExtendAuthSessionRequest kicks off the session-extension SSO flow. +message RequestExtendAuthSessionRequest { + // Optional OIDC login_hint (typically the user's email) to pre-fill the + // IdP login form. + optional string hint = 1; +} + +// RequestExtendAuthSessionResponse carries the verification URI the UI +// should open in a browser. The daemon retains the flow state and resolves +// it via WaitExtendAuthSession. +message RequestExtendAuthSessionResponse { + // verification URI for the user to open in the browser + string verificationURI = 1; + // complete verification URI (with embedded user code) + string verificationURIComplete = 2; + // user code to enter on verification URI (for device-code flows) + string userCode = 3; + // device code for matching the WaitExtendAuthSession call to this flow + string deviceCode = 4; + // expiration time in seconds for the device code / PKCE flow + int64 expiresIn = 5; +} + +// WaitExtendAuthSessionRequest is sent by the UI after it opens the +// verification URI. The daemon blocks on this call until the user +// completes (or aborts) the SSO step. +message WaitExtendAuthSessionRequest { + // device code returned by RequestExtendAuthSession + string deviceCode = 1; + // user code for verification + string userCode = 2; +} + +// WaitExtendAuthSessionResponse carries the refreshed deadline returned +// by the management server. Unset when the management server reports the +// peer is not eligible for session extension. +message WaitExtendAuthSessionResponse { + google.protobuf.Timestamp sessionExpiresAt = 1; +} + +// DismissSessionWarningRequest is sent by the UI when the user clicks +// "Dismiss" on the T-WarningLead notification. +message DismissSessionWarningRequest {} + +// DismissSessionWarningResponse acknowledges the dismissal. Carries no +// payload — the daemon's only obligation is to silence the upcoming +// T-FinalWarningLead fallback for the current deadline. +message DismissSessionWarningResponse {} + // StartCPUProfileRequest for starting CPU profiling message StartCPUProfileRequest {} diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 5f585aafc..2d01d474d 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -23,6 +23,7 @@ const ( DaemonService_WaitSSOLogin_FullMethodName = "/daemon.DaemonService/WaitSSOLogin" DaemonService_Up_FullMethodName = "/daemon.DaemonService/Up" DaemonService_Status_FullMethodName = "/daemon.DaemonService/Status" + DaemonService_SubscribeStatus_FullMethodName = "/daemon.DaemonService/SubscribeStatus" DaemonService_Down_FullMethodName = "/daemon.DaemonService/Down" DaemonService_GetConfig_FullMethodName = "/daemon.DaemonService/GetConfig" DaemonService_ListNetworks_FullMethodName = "/daemon.DaemonService/ListNetworks" @@ -42,6 +43,7 @@ const ( DaemonService_StopBundleCapture_FullMethodName = "/daemon.DaemonService/StopBundleCapture" DaemonService_SubscribeEvents_FullMethodName = "/daemon.DaemonService/SubscribeEvents" DaemonService_GetEvents_FullMethodName = "/daemon.DaemonService/GetEvents" + DaemonService_RegisterUILog_FullMethodName = "/daemon.DaemonService/RegisterUILog" DaemonService_SwitchProfile_FullMethodName = "/daemon.DaemonService/SwitchProfile" DaemonService_SetConfig_FullMethodName = "/daemon.DaemonService/SetConfig" DaemonService_AddProfile_FullMethodName = "/daemon.DaemonService/AddProfile" @@ -55,10 +57,14 @@ const ( DaemonService_GetPeerSSHHostKey_FullMethodName = "/daemon.DaemonService/GetPeerSSHHostKey" DaemonService_RequestJWTAuth_FullMethodName = "/daemon.DaemonService/RequestJWTAuth" DaemonService_WaitJWTToken_FullMethodName = "/daemon.DaemonService/WaitJWTToken" + DaemonService_RequestExtendAuthSession_FullMethodName = "/daemon.DaemonService/RequestExtendAuthSession" + DaemonService_WaitExtendAuthSession_FullMethodName = "/daemon.DaemonService/WaitExtendAuthSession" + DaemonService_DismissSessionWarning_FullMethodName = "/daemon.DaemonService/DismissSessionWarning" DaemonService_StartCPUProfile_FullMethodName = "/daemon.DaemonService/StartCPUProfile" DaemonService_StopCPUProfile_FullMethodName = "/daemon.DaemonService/StopCPUProfile" DaemonService_GetInstallerResult_FullMethodName = "/daemon.DaemonService/GetInstallerResult" DaemonService_ExposeService_FullMethodName = "/daemon.DaemonService/ExposeService" + DaemonService_WailsUIReady_FullMethodName = "/daemon.DaemonService/WailsUIReady" ) // DaemonServiceClient is the client API for DaemonService service. @@ -74,6 +80,11 @@ type DaemonServiceClient interface { Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error) // Status of the service. Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) + // SubscribeStatus pushes a fresh StatusResponse on connection state + // changes (Connected / Disconnected / Connecting / address change / + // peers list change). The first message on the stream is the current + // snapshot, so a freshly-subscribed UI doesn't need to also call Status. + SubscribeStatus(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StatusResponse], error) // Down stops engine work in the daemon. Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) // GetConfig of the daemon. @@ -110,6 +121,10 @@ type DaemonServiceClient interface { StopBundleCapture(ctx context.Context, in *StopBundleCaptureRequest, opts ...grpc.CallOption) (*StopBundleCaptureResponse, error) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SystemEvent], error) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) + // RegisterUILog records the desktop UI's absolute log path so the daemon's + // debug bundle can collect it (the daemon runs as root and can't resolve the + // user's config dir). + RegisterUILog(ctx context.Context, in *RegisterUILogRequest, opts ...grpc.CallOption) (*RegisterUILogResponse, error) SwitchProfile(ctx context.Context, in *SwitchProfileRequest, opts ...grpc.CallOption) (*SwitchProfileResponse, error) SetConfig(ctx context.Context, in *SetConfigRequest, opts ...grpc.CallOption) (*SetConfigResponse, error) AddProfile(ctx context.Context, in *AddProfileRequest, opts ...grpc.CallOption) (*AddProfileResponse, error) @@ -129,6 +144,22 @@ type DaemonServiceClient interface { RequestJWTAuth(ctx context.Context, in *RequestJWTAuthRequest, opts ...grpc.CallOption) (*RequestJWTAuthResponse, error) // WaitJWTToken waits for JWT authentication completion WaitJWTToken(ctx context.Context, in *WaitJWTTokenRequest, opts ...grpc.CallOption) (*WaitJWTTokenResponse, error) + // RequestExtendAuthSession initiates an SSO session-extension flow. + // The daemon prepares a PKCE/device-code request against the IdP and + // returns the verification URI; the UI is expected to open it. The flow + // state is kept in the daemon until WaitExtendAuthSession completes it. + RequestExtendAuthSession(ctx context.Context, in *RequestExtendAuthSessionRequest, opts ...grpc.CallOption) (*RequestExtendAuthSessionResponse, error) + // WaitExtendAuthSession blocks until the user finishes the SSO step + // started by RequestExtendAuthSession, then forwards the resulting JWT + // to the management server's ExtendAuthSession RPC. Returns the new + // session expiry deadline. The tunnel stays up the entire time. + WaitExtendAuthSession(ctx context.Context, in *WaitExtendAuthSessionRequest, opts ...grpc.CallOption) (*WaitExtendAuthSessionResponse, error) + // DismissSessionWarning records that the user clicked "Dismiss" on the + // T-WarningLead interactive notification, suppressing the auto-opened + // SessionAboutToExpire dialog that would otherwise fire at + // T-FinalWarningLead for the current deadline. Idempotent and best-effort: + // a missed call only means the fallback dialog will still appear. + DismissSessionWarning(ctx context.Context, in *DismissSessionWarningRequest, opts ...grpc.CallOption) (*DismissSessionWarningResponse, error) // StartCPUProfile starts CPU profiling in the daemon StartCPUProfile(ctx context.Context, in *StartCPUProfileRequest, opts ...grpc.CallOption) (*StartCPUProfileResponse, error) // StopCPUProfile stops CPU profiling in the daemon @@ -136,6 +167,10 @@ type DaemonServiceClient interface { GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error) // ExposeService exposes a local port via the NetBird reverse proxy ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExposeServiceEvent], error) + // WailsUIReady is a no-op probe the Wails UI calls once at startup. The UI + // only cares whether the daemon implements it: an Unimplemented response + // means the daemon predates this UI and is too old to drive it. + WailsUIReady(ctx context.Context, in *WailsUIReadyRequest, opts ...grpc.CallOption) (*WailsUIReadyResponse, error) } type daemonServiceClient struct { @@ -186,6 +221,25 @@ func (c *daemonServiceClient) Status(ctx context.Context, in *StatusRequest, opt return out, nil } +func (c *daemonServiceClient) SubscribeStatus(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StatusResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], DaemonService_SubscribeStatus_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[StatusRequest, StatusResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DaemonService_SubscribeStatusClient = grpc.ServerStreamingClient[StatusResponse] + func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DownResponse) @@ -328,7 +382,7 @@ func (c *daemonServiceClient) TracePacket(ctx context.Context, in *TracePacketRe func (c *daemonServiceClient) StartCapture(ctx context.Context, in *StartCaptureRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CapturePacket], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], DaemonService_StartCapture_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[1], DaemonService_StartCapture_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -367,7 +421,7 @@ func (c *daemonServiceClient) StopBundleCapture(ctx context.Context, in *StopBun func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SystemEvent], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[1], DaemonService_SubscribeEvents_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[2], DaemonService_SubscribeEvents_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -394,6 +448,16 @@ func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsReques return out, nil } +func (c *daemonServiceClient) RegisterUILog(ctx context.Context, in *RegisterUILogRequest, opts ...grpc.CallOption) (*RegisterUILogResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RegisterUILogResponse) + err := c.cc.Invoke(ctx, DaemonService_RegisterUILog_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *daemonServiceClient) SwitchProfile(ctx context.Context, in *SwitchProfileRequest, opts ...grpc.CallOption) (*SwitchProfileResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SwitchProfileResponse) @@ -524,6 +588,36 @@ func (c *daemonServiceClient) WaitJWTToken(ctx context.Context, in *WaitJWTToken return out, nil } +func (c *daemonServiceClient) RequestExtendAuthSession(ctx context.Context, in *RequestExtendAuthSessionRequest, opts ...grpc.CallOption) (*RequestExtendAuthSessionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RequestExtendAuthSessionResponse) + err := c.cc.Invoke(ctx, DaemonService_RequestExtendAuthSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) WaitExtendAuthSession(ctx context.Context, in *WaitExtendAuthSessionRequest, opts ...grpc.CallOption) (*WaitExtendAuthSessionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WaitExtendAuthSessionResponse) + err := c.cc.Invoke(ctx, DaemonService_WaitExtendAuthSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) DismissSessionWarning(ctx context.Context, in *DismissSessionWarningRequest, opts ...grpc.CallOption) (*DismissSessionWarningResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DismissSessionWarningResponse) + err := c.cc.Invoke(ctx, DaemonService_DismissSessionWarning_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *daemonServiceClient) StartCPUProfile(ctx context.Context, in *StartCPUProfileRequest, opts ...grpc.CallOption) (*StartCPUProfileResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(StartCPUProfileResponse) @@ -556,7 +650,7 @@ func (c *daemonServiceClient) GetInstallerResult(ctx context.Context, in *Instal func (c *daemonServiceClient) ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExposeServiceEvent], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[2], DaemonService_ExposeService_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[3], DaemonService_ExposeService_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -573,6 +667,16 @@ func (c *daemonServiceClient) ExposeService(ctx context.Context, in *ExposeServi // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type DaemonService_ExposeServiceClient = grpc.ServerStreamingClient[ExposeServiceEvent] +func (c *daemonServiceClient) WailsUIReady(ctx context.Context, in *WailsUIReadyRequest, opts ...grpc.CallOption) (*WailsUIReadyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WailsUIReadyResponse) + err := c.cc.Invoke(ctx, DaemonService_WailsUIReady_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility. @@ -586,6 +690,11 @@ type DaemonServiceServer interface { Up(context.Context, *UpRequest) (*UpResponse, error) // Status of the service. Status(context.Context, *StatusRequest) (*StatusResponse, error) + // SubscribeStatus pushes a fresh StatusResponse on connection state + // changes (Connected / Disconnected / Connecting / address change / + // peers list change). The first message on the stream is the current + // snapshot, so a freshly-subscribed UI doesn't need to also call Status. + SubscribeStatus(*StatusRequest, grpc.ServerStreamingServer[StatusResponse]) error // Down stops engine work in the daemon. Down(context.Context, *DownRequest) (*DownResponse, error) // GetConfig of the daemon. @@ -622,6 +731,10 @@ type DaemonServiceServer interface { StopBundleCapture(context.Context, *StopBundleCaptureRequest) (*StopBundleCaptureResponse, error) SubscribeEvents(*SubscribeRequest, grpc.ServerStreamingServer[SystemEvent]) error GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) + // RegisterUILog records the desktop UI's absolute log path so the daemon's + // debug bundle can collect it (the daemon runs as root and can't resolve the + // user's config dir). + RegisterUILog(context.Context, *RegisterUILogRequest) (*RegisterUILogResponse, error) SwitchProfile(context.Context, *SwitchProfileRequest) (*SwitchProfileResponse, error) SetConfig(context.Context, *SetConfigRequest) (*SetConfigResponse, error) AddProfile(context.Context, *AddProfileRequest) (*AddProfileResponse, error) @@ -641,6 +754,22 @@ type DaemonServiceServer interface { RequestJWTAuth(context.Context, *RequestJWTAuthRequest) (*RequestJWTAuthResponse, error) // WaitJWTToken waits for JWT authentication completion WaitJWTToken(context.Context, *WaitJWTTokenRequest) (*WaitJWTTokenResponse, error) + // RequestExtendAuthSession initiates an SSO session-extension flow. + // The daemon prepares a PKCE/device-code request against the IdP and + // returns the verification URI; the UI is expected to open it. The flow + // state is kept in the daemon until WaitExtendAuthSession completes it. + RequestExtendAuthSession(context.Context, *RequestExtendAuthSessionRequest) (*RequestExtendAuthSessionResponse, error) + // WaitExtendAuthSession blocks until the user finishes the SSO step + // started by RequestExtendAuthSession, then forwards the resulting JWT + // to the management server's ExtendAuthSession RPC. Returns the new + // session expiry deadline. The tunnel stays up the entire time. + WaitExtendAuthSession(context.Context, *WaitExtendAuthSessionRequest) (*WaitExtendAuthSessionResponse, error) + // DismissSessionWarning records that the user clicked "Dismiss" on the + // T-WarningLead interactive notification, suppressing the auto-opened + // SessionAboutToExpire dialog that would otherwise fire at + // T-FinalWarningLead for the current deadline. Idempotent and best-effort: + // a missed call only means the fallback dialog will still appear. + DismissSessionWarning(context.Context, *DismissSessionWarningRequest) (*DismissSessionWarningResponse, error) // StartCPUProfile starts CPU profiling in the daemon StartCPUProfile(context.Context, *StartCPUProfileRequest) (*StartCPUProfileResponse, error) // StopCPUProfile stops CPU profiling in the daemon @@ -648,6 +777,10 @@ type DaemonServiceServer interface { GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) // ExposeService exposes a local port via the NetBird reverse proxy ExposeService(*ExposeServiceRequest, grpc.ServerStreamingServer[ExposeServiceEvent]) error + // WailsUIReady is a no-op probe the Wails UI calls once at startup. The UI + // only cares whether the daemon implements it: an Unimplemented response + // means the daemon predates this UI and is too old to drive it. + WailsUIReady(context.Context, *WailsUIReadyRequest) (*WailsUIReadyResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -670,6 +803,9 @@ func (UnimplementedDaemonServiceServer) Up(context.Context, *UpRequest) (*UpResp func (UnimplementedDaemonServiceServer) Status(context.Context, *StatusRequest) (*StatusResponse, error) { return nil, status.Error(codes.Unimplemented, "method Status not implemented") } +func (UnimplementedDaemonServiceServer) SubscribeStatus(*StatusRequest, grpc.ServerStreamingServer[StatusResponse]) error { + return status.Error(codes.Unimplemented, "method SubscribeStatus not implemented") +} func (UnimplementedDaemonServiceServer) Down(context.Context, *DownRequest) (*DownResponse, error) { return nil, status.Error(codes.Unimplemented, "method Down not implemented") } @@ -727,6 +863,9 @@ func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, grpc. func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetEvents not implemented") } +func (UnimplementedDaemonServiceServer) RegisterUILog(context.Context, *RegisterUILogRequest) (*RegisterUILogResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RegisterUILog not implemented") +} func (UnimplementedDaemonServiceServer) SwitchProfile(context.Context, *SwitchProfileRequest) (*SwitchProfileResponse, error) { return nil, status.Error(codes.Unimplemented, "method SwitchProfile not implemented") } @@ -766,6 +905,15 @@ func (UnimplementedDaemonServiceServer) RequestJWTAuth(context.Context, *Request func (UnimplementedDaemonServiceServer) WaitJWTToken(context.Context, *WaitJWTTokenRequest) (*WaitJWTTokenResponse, error) { return nil, status.Error(codes.Unimplemented, "method WaitJWTToken not implemented") } +func (UnimplementedDaemonServiceServer) RequestExtendAuthSession(context.Context, *RequestExtendAuthSessionRequest) (*RequestExtendAuthSessionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RequestExtendAuthSession not implemented") +} +func (UnimplementedDaemonServiceServer) WaitExtendAuthSession(context.Context, *WaitExtendAuthSessionRequest) (*WaitExtendAuthSessionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method WaitExtendAuthSession not implemented") +} +func (UnimplementedDaemonServiceServer) DismissSessionWarning(context.Context, *DismissSessionWarningRequest) (*DismissSessionWarningResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DismissSessionWarning not implemented") +} func (UnimplementedDaemonServiceServer) StartCPUProfile(context.Context, *StartCPUProfileRequest) (*StartCPUProfileResponse, error) { return nil, status.Error(codes.Unimplemented, "method StartCPUProfile not implemented") } @@ -778,6 +926,9 @@ func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *Ins func (UnimplementedDaemonServiceServer) ExposeService(*ExposeServiceRequest, grpc.ServerStreamingServer[ExposeServiceEvent]) error { return status.Error(codes.Unimplemented, "method ExposeService not implemented") } +func (UnimplementedDaemonServiceServer) WailsUIReady(context.Context, *WailsUIReadyRequest) (*WailsUIReadyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method WailsUIReady not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} func (UnimplementedDaemonServiceServer) testEmbeddedByValue() {} @@ -871,6 +1022,17 @@ func _DaemonService_Status_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _DaemonService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(StatusRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(DaemonServiceServer).SubscribeStatus(m, &grpc.GenericServerStream[StatusRequest, StatusResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DaemonService_SubscribeStatusServer = grpc.ServerStreamingServer[StatusResponse] + func _DaemonService_Down_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DownRequest) if err := dec(in); err != nil { @@ -1199,6 +1361,24 @@ func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _DaemonService_RegisterUILog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterUILogRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).RegisterUILog(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_RegisterUILog_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).RegisterUILog(ctx, req.(*RegisterUILogRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _DaemonService_SwitchProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SwitchProfileRequest) if err := dec(in); err != nil { @@ -1433,6 +1613,60 @@ func _DaemonService_WaitJWTToken_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _DaemonService_RequestExtendAuthSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RequestExtendAuthSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).RequestExtendAuthSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_RequestExtendAuthSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).RequestExtendAuthSession(ctx, req.(*RequestExtendAuthSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_WaitExtendAuthSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WaitExtendAuthSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).WaitExtendAuthSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_WaitExtendAuthSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).WaitExtendAuthSession(ctx, req.(*WaitExtendAuthSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_DismissSessionWarning_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DismissSessionWarningRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).DismissSessionWarning(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_DismissSessionWarning_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).DismissSessionWarning(ctx, req.(*DismissSessionWarningRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _DaemonService_StartCPUProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(StartCPUProfileRequest) if err := dec(in); err != nil { @@ -1498,6 +1732,24 @@ func _DaemonService_ExposeService_Handler(srv interface{}, stream grpc.ServerStr // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type DaemonService_ExposeServiceServer = grpc.ServerStreamingServer[ExposeServiceEvent] +func _DaemonService_WailsUIReady_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WailsUIReadyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).WailsUIReady(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_WailsUIReady_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).WailsUIReady(ctx, req.(*WailsUIReadyRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1589,6 +1841,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetEvents", Handler: _DaemonService_GetEvents_Handler, }, + { + MethodName: "RegisterUILog", + Handler: _DaemonService_RegisterUILog_Handler, + }, { MethodName: "SwitchProfile", Handler: _DaemonService_SwitchProfile_Handler, @@ -1641,6 +1897,18 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "WaitJWTToken", Handler: _DaemonService_WaitJWTToken_Handler, }, + { + MethodName: "RequestExtendAuthSession", + Handler: _DaemonService_RequestExtendAuthSession_Handler, + }, + { + MethodName: "WaitExtendAuthSession", + Handler: _DaemonService_WaitExtendAuthSession_Handler, + }, + { + MethodName: "DismissSessionWarning", + Handler: _DaemonService_DismissSessionWarning_Handler, + }, { MethodName: "StartCPUProfile", Handler: _DaemonService_StartCPUProfile_Handler, @@ -1653,8 +1921,17 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetInstallerResult", Handler: _DaemonService_GetInstallerResult_Handler, }, + { + MethodName: "WailsUIReady", + Handler: _DaemonService_WailsUIReady_Handler, + }, }, Streams: []grpc.StreamDesc{ + { + StreamName: "SubscribeStatus", + Handler: _DaemonService_SubscribeStatus_Handler, + ServerStreams: true, + }, { StreamName: "StartCapture", Handler: _DaemonService_StartCapture_Handler, diff --git a/client/proto/metadata.go b/client/proto/metadata.go new file mode 100644 index 000000000..9b1dbd16e --- /dev/null +++ b/client/proto/metadata.go @@ -0,0 +1,61 @@ +package proto + +// SystemEvent metadata markers. The daemon stamps these on internal control +// events it publishes over SubscribeEvents (profile-list refresh, log-level +// change); the desktop UI recognises them and acts on them instead of +// surfacing them as user-facing notifications. +// +// These live in the proto package — the shared contract both the daemon +// (client/server) and the UI (client/ui/services) already import — so producer +// and consumer reference the same constant rather than duplicating literals. +// This file is hand-written and not touched by protoc. +const ( + // MetadataKindKey is the SystemEvent.metadata key carrying the event-kind + // marker (one of the MetadataKind* values below). + MetadataKindKey = "kind" + + // MetadataKindProfileListChanged marks a CLI-driven profile add/remove that + // should nudge the UI's profile views to refresh. + MetadataKindProfileListChanged = "profile-list-changed" + // MetadataKindLogLevelChanged marks a daemon log-level change (or the + // per-subscription snapshot) that drives the GUI's file logging on/off. + MetadataKindLogLevelChanged = "log-level-changed" + + // MetadataProfileKey carries the profile name for + // MetadataKindProfileListChanged. + MetadataProfileKey = "profile" + // MetadataLevelKey carries the lowercase logrus level name for + // MetadataKindLogLevelChanged. + MetadataLevelKey = "level" +) + +// SystemEvent metadata markers for daemon config-change events. The daemon +// publishes a SYSTEM-category event whenever its effective Config is +// replaced (engine spawn, Up RPC, MDM policy diff); the UI re-fetches its +// cached config/features in response and, for the MDM source, shows a +// localised toast. Producer (client/server) and consumer (client/ui) share +// these so neither duplicates the wire literals. +const ( + // MetadataTypeKey is the SystemEvent.metadata key carrying the + // config-change event type (one of the MetadataType* values below). + MetadataTypeKey = "type" + // MetadataTypeConfigChanged marks a config replacement that should nudge + // UIs to re-fetch their cached config + features. UserMessage is empty so + // the change is silent; the source is carried in MetadataSourceKey. + MetadataTypeConfigChanged = "config_changed" + // MetadataTypePolicyApplied marks an MDM-policy-driven config change. The + // daemon stamps it with a (non-localised) UserMessage; the UI suppresses + // that and builds its own localised toast off the paired config_changed + // event instead. + MetadataTypePolicyApplied = "policy_applied" + + // MetadataSourceKey is the SystemEvent.metadata key carrying what + // triggered a config_changed event (one of the MetadataSource* values). + MetadataSourceKey = "source" + // MetadataSourceStartup marks a config_changed from the daemon Start path. + MetadataSourceStartup = "startup" + // MetadataSourceUpRPC marks a config_changed from the Up RPC. + MetadataSourceUpRPC = "up_rpc" + // MetadataSourceMDM marks a config_changed driven by an MDM policy diff. + MetadataSourceMDM = "mdm" +) diff --git a/client/server/debug.go b/client/server/debug.go index 14dcaba33..0b6ac4b53 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -53,7 +53,10 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) ( if engine != nil { refreshStatus = func() { log.Debug("refreshing system health status for debug bundle") - engine.RunHealthProbes(true) + // Background ctx: the bundle wants a full, fresh probe regardless + // of the DebugBundle RPC client's lifetime. The engine's own ctx + // still aborts it on shutdown. + engine.RunHealthProbes(context.Background(), true) } } } @@ -64,6 +67,7 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) ( StatusRecorder: s.statusRecorder, SyncResponse: syncResponse, LogPath: s.logFile, + UILogPath: s.uiLogPath, CPUProfile: cpuProfileData, CapturePath: capturePath, RefreshStatus: refreshStatus, @@ -124,9 +128,26 @@ func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) ( log.Infof("Log level set to %s", level.String()) + // Signal the desktop UI so it can attach/detach its gui-client.log. Rides + // the SubscribeEvents stream as a marked event (see publishLogLevelChanged). + s.publishLogLevelChanged(level.String()) + return &proto.SetLogLevelResponse{}, nil } +// RegisterUILog records the desktop UI's absolute log path so DebugBundle can +// collect the GUI log. The daemon runs as root and can't resolve the user's +// config dir, so the UI reports it. Last-writer-wins (one UI per socket). +func (s *Server) RegisterUILog(_ context.Context, req *proto.RegisterUILogRequest) (*proto.RegisterUILogResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + s.uiLogPath = req.GetPath() + log.Infof("registered UI log path: %s", s.uiLogPath) + + return &proto.RegisterUILogResponse{}, nil +} + // SetSyncResponsePersistence sets the sync response persistence for the server. func (s *Server) SetSyncResponsePersistence(_ context.Context, req *proto.SetSyncResponsePersistenceRequest) (*proto.SetSyncResponsePersistenceResponse, error) { s.mutex.Lock() diff --git a/client/server/event.go b/client/server/event.go index d93151c96..753a051e7 100644 --- a/client/server/event.go +++ b/client/server/event.go @@ -1,7 +1,9 @@ package server import ( + "github.com/google/uuid" log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/netbirdio/netbird/client/proto" ) @@ -16,6 +18,15 @@ func (s *Server) SubscribeEvents(req *proto.SubscribeRequest, stream proto.Daemo log.Debug("client subscribed to events") s.startUpdateManagerForGUI() + // Replay the current log level to this subscriber so a freshly-connected UI + // learns it even when the daemon was already started with --log-level debug + // (the change-driven publishLogLevelChanged only fires on SetLogLevel). Sent + // directly on this stream rather than via PublishEvent so it reaches only + // the new subscriber, not every connected client. + if err := s.sendCurrentLogLevel(stream); err != nil { + return err + } + for { select { case event := <-subscription.Events(): @@ -28,3 +39,24 @@ func (s *Server) SubscribeEvents(req *proto.SubscribeRequest, stream proto.Daemo } } } + +// sendCurrentLogLevel sends a marked log-level-changed SystemEvent carrying the +// daemon's current level directly to one subscriber. Mirrors the shape +// publishLogLevelChanged emits so the UI's dispatchSystemEvent handles both the +// same way. +func (s *Server) sendCurrentLogLevel(stream proto.DaemonService_SubscribeEventsServer) error { + level := log.GetLevel().String() + event := &proto.SystemEvent{ + Id: uuid.New().String(), + Severity: proto.SystemEvent_INFO, + Category: proto.SystemEvent_SYSTEM, + Message: "Log level changed", + Metadata: map[string]string{proto.MetadataKindKey: proto.MetadataKindLogLevelChanged, proto.MetadataLevelKey: level}, + Timestamp: timestamppb.Now(), + } + if err := stream.Send(event); err != nil { + log.Warnf("error sending initial log level event: %v", err) + return err + } + return nil +} diff --git a/client/server/extend_authsession_test.go b/client/server/extend_authsession_test.go new file mode 100644 index 000000000..a1a048a7c --- /dev/null +++ b/client/server/extend_authsession_test.go @@ -0,0 +1,42 @@ +package server + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" +) + +func TestInnermostStatus(t *testing.T) { + t.Run("wrapped gRPC status", func(t *testing.T) { + inner := gstatus.Error(codes.PermissionDenied, "peer is already registered by a different User or a Setup Key") + // Mirror the daemon wrap chain: engine wraps with %w, mgm error is the inner status. + wrapped := fmt.Errorf("extend auth session on management: %w", inner) + + st := innermostStatus(wrapped) + require.NotNil(t, st) + require.Equal(t, codes.PermissionDenied, st.Code()) + require.Equal(t, "peer is already registered by a different User or a Setup Key", st.Message()) + }) + + t.Run("deepest status wins over an outer one", func(t *testing.T) { + inner := gstatus.Error(codes.PermissionDenied, "deepest") + chain := fmt.Errorf("outer: %w", fmt.Errorf("mid: %w", inner)) + + st := innermostStatus(chain) + require.NotNil(t, st) + require.Equal(t, codes.PermissionDenied, st.Code()) + require.Equal(t, "deepest", st.Message()) + }) + + t.Run("no status in chain", func(t *testing.T) { + require.Nil(t, innermostStatus(errors.New("plain error"))) + }) + + t.Run("nil error", func(t *testing.T) { + require.Nil(t, innermostStatus(nil)) + }) +} diff --git a/client/server/mdm.go b/client/server/mdm.go index 0da0ec5d1..547baf8b8 100644 --- a/client/server/mdm.go +++ b/client/server/mdm.go @@ -99,7 +99,10 @@ func (s *Server) onMDMPolicyChange(_, _ *mdm.Policy) error { proto.SystemEvent_SYSTEM, "MDM policy applied", "NetBird configuration was updated by your IT policy.", - map[string]string{"source": "mdm", "type": "policy_applied"}, + map[string]string{ + proto.MetadataSourceKey: proto.MetadataSourceMDM, + proto.MetadataTypeKey: proto.MetadataTypePolicyApplied, + }, ) return nil } @@ -124,8 +127,8 @@ func (s *Server) publishConfigChangedEvent(source string) { fmt.Sprintf("daemon config changed (source=%s)", source), "", map[string]string{ - "source": source, - "type": "config_changed", + proto.MetadataSourceKey: source, + proto.MetadataTypeKey: proto.MetadataTypeConfigChanged, }, ) } @@ -161,7 +164,7 @@ func (s *Server) restartEngineForMDMLocked() error { s.clientGiveUpChan = make(chan struct{}) log.Info("MDM restart: spawning connectWithRetryRuns with re-resolved config") go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.clientRunningChan, s.clientGiveUpChan) - s.publishConfigChangedEvent("mdm") + s.publishConfigChangedEvent(proto.MetadataSourceMDM) return nil } diff --git a/client/server/network.go b/client/server/network.go index 7a3c08f2e..c38715256 100644 --- a/client/server/network.go +++ b/client/server/network.go @@ -172,6 +172,17 @@ func (s *Server) SelectNetworks(_ context.Context, req *proto.SelectNetworksRequ if err := routeSelector.SelectRoutes(routes, req.GetAppend(), netIdRoutes); err != nil { return nil, fmt.Errorf("select routes: %w", err) } + + // Exit nodes are mutually exclusive: if this selection activates an + // exit node, deselect every other available exit node so two can't be + // selected at once. Non-exit route selections are left untouched. + if requestActivatesExitNode(routes, routesMap) { + if others := otherExitNodeIDs(routesMap, routes); len(others) > 0 { + if err := routeSelector.DeselectRoutes(others, netIdRoutes); err != nil { + return nil, fmt.Errorf("deselect sibling exit nodes: %w", err) + } + } + } } routeManager.TriggerSelection(routeManager.GetClientRoutes()) @@ -249,3 +260,38 @@ func toNetIDs(routes []string) []route.NetID { } return netIDs } + +func isExitNodeRoutes(routes []*route.Route) bool { + return len(routes) > 0 && (route.IsV4DefaultRoute(routes[0].Network) || route.IsV6DefaultRoute(routes[0].Network)) +} + +// requestActivatesExitNode reports whether any requested NetID maps to an exit +// node (default route) in the current route table. +func requestActivatesExitNode(requested []route.NetID, routesMap map[route.NetID][]*route.Route) bool { + for _, id := range requested { + if isExitNodeRoutes(routesMap[id]) { + return true + } + } + return false +} + +// otherExitNodeIDs returns every available exit-node NetID that is not in the +// requested set — the siblings to deselect so a single exit node stays active. +func otherExitNodeIDs(routesMap map[route.NetID][]*route.Route, requested []route.NetID) []route.NetID { + keep := make(map[route.NetID]struct{}, len(requested)) + for _, id := range requested { + keep[id] = struct{}{} + } + var others []route.NetID + for id, routes := range routesMap { + if !isExitNodeRoutes(routes) { + continue + } + if _, ok := keep[id]; ok { + continue + } + others = append(others, id) + } + return others +} diff --git a/client/server/network_exitnode_test.go b/client/server/network_exitnode_test.go new file mode 100644 index 000000000..1c0ba0ecb --- /dev/null +++ b/client/server/network_exitnode_test.go @@ -0,0 +1,26 @@ +package server + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netbirdio/netbird/route" +) + +func TestExitNodeSelectionHelpers(t *testing.T) { + routesMap := map[route.NetID][]*route.Route{ + "exitA": {{Network: netip.MustParsePrefix("0.0.0.0/0")}}, + "exitB": {{Network: netip.MustParsePrefix("::/0")}}, + "lan": {{Network: netip.MustParsePrefix("192.168.0.0/16")}}, + } + + assert.True(t, requestActivatesExitNode([]route.NetID{"exitA"}, routesMap), "v4 default route is an exit node") + assert.True(t, requestActivatesExitNode([]route.NetID{"exitB"}, routesMap), "v6 default route is an exit node") + assert.False(t, requestActivatesExitNode([]route.NetID{"lan"}, routesMap), "lan route is not an exit node") + assert.False(t, requestActivatesExitNode([]route.NetID{"missing"}, routesMap), "unknown id is not an exit node") + + others := otherExitNodeIDs(routesMap, []route.NetID{"exitB"}) + assert.ElementsMatch(t, []route.NetID{"exitA"}, others, "only the other exit node is a sibling; the lan route is ignored") +} diff --git a/client/server/probe_throttle.go b/client/server/probe_throttle.go new file mode 100644 index 000000000..ec6137e15 --- /dev/null +++ b/client/server/probe_throttle.go @@ -0,0 +1,88 @@ +package server + +import ( + "context" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +// healthProbeRunner runs the full, expensive probe (network round-trips to +// management, signal and the relays) and reports whether every component was +// healthy. ctx cancels the probe when the caller gives up. Satisfied by +// *internal.Engine. +type healthProbeRunner interface { + RunHealthProbes(ctx context.Context, waitForResult bool) bool +} + +// statsRefresher does the cheap WireGuard-stats refresh callers fall back to +// when a fresh probe isn't warranted. Satisfied by *peer.Status. +type statsRefresher interface { + RefreshWireGuardStats() error +} + +// probeThrottle rate-limits and single-flights the daemon's health probes. +// +// Health probes are expensive (network round-trips to management, signal and +// the relays), while Status(GetFullPeerStatus=true) RPCs can arrive frequently +// and concurrently — the desktop UI alone issues one per connect/disconnect. +// probeThrottle keeps that load bounded with two rules: +// +// - Single-flight: only one probe runs at a time. Callers that pile up while +// a probe is in flight share its result instead of each launching another, +// even when that probe failed. A failed probe therefore does not make every +// waiter re-probe in turn; the next, non-overlapping caller can try again. +// - Throttle: after a fully successful probe the result is cached for +// interval. While any component is unhealthy the cache is not advanced, so +// later callers keep probing frequently and notice recovery quickly — the +// intentional "probe often while unhealthy" behaviour from the original +// design. +type probeThrottle struct { + interval time.Duration + + mu sync.Mutex + lastOK time.Time // last fully-successful probe; drives the throttle window + completedAt time.Time // when the most recent probe finished; drives single-flight sharing +} + +func newProbeThrottle(interval time.Duration) *probeThrottle { + return &probeThrottle{interval: interval} +} + +// Run decides whether to run a fresh health probe or serve the most recent +// result. It serialises concurrent callers: at most one runner.RunHealthProbes +// executes at a time and the rest call refresher.RefreshWireGuardStats and read +// the snapshot it produced. +// +// Both calls run while the throttle's lock is held, so a slow probe blocks +// other callers until it completes — that blocking is the single-flight +// guarantee. ctx is forwarded to RunHealthProbes so a caller that gives up +// cancels the in-flight probe (and any caller still queued on the lock falls +// through quickly once it acquires it, since the probe ctx is already done). +func (t *probeThrottle) Run(ctx context.Context, runner healthProbeRunner, refresher statsRefresher, waitForResult bool) { + entered := time.Now() + + t.mu.Lock() + defer t.mu.Unlock() + + // A probe that finished after we entered ran while we were waiting on the + // lock — i.e. a peer in the same burst already probed for us, so share its + // result rather than launch another. This holds even when that probe + // failed, so a failed probe doesn't make every waiter re-probe in turn. + sharedRecentProbe := t.completedAt.After(entered) + throttled := time.Since(t.lastOK) <= t.interval + + if sharedRecentProbe || throttled { + if err := refresher.RefreshWireGuardStats(); err != nil { + log.Debugf("failed to refresh WireGuard stats: %v", err) + } + return + } + + healthy := runner.RunHealthProbes(ctx, waitForResult) + t.completedAt = time.Now() + if healthy { + t.lastOK = t.completedAt + } +} diff --git a/client/server/probe_throttle_test.go b/client/server/probe_throttle_test.go new file mode 100644 index 000000000..cae776fa4 --- /dev/null +++ b/client/server/probe_throttle_test.go @@ -0,0 +1,109 @@ +package server + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" +) + +// fakeProber implements both healthProbeRunner and statsRefresher with +// caller-supplied behaviour. +type fakeProber struct { + onProbe func() bool + onRefresh func() +} + +func (f fakeProber) RunHealthProbes(context.Context, bool) bool { + return f.onProbe() +} + +func (f fakeProber) RefreshWireGuardStats() error { + if f.onRefresh != nil { + f.onRefresh() + } + return nil +} + +func TestProbeThrottle_CachesAfterSuccess(t *testing.T) { + pt := newProbeThrottle(time.Minute) + + var probes, refreshes int + prober := fakeProber{ + onProbe: func() bool { probes++; return true }, + onRefresh: func() { refreshes++ }, + } + + pt.Run(context.Background(), prober, prober, false) + pt.Run(context.Background(), prober, prober, false) + + if probes != 1 { + t.Fatalf("expected 1 probe within the throttle window, got %d", probes) + } + if refreshes != 1 { + t.Fatalf("expected the throttled caller to refresh stats once, got %d", refreshes) + } +} + +func TestProbeThrottle_StaysOpenWhileUnhealthy(t *testing.T) { + pt := newProbeThrottle(time.Minute) + + var probes int + prober := fakeProber{onProbe: func() bool { probes++; return false }} // never healthy + + // Sequential, non-overlapping callers must each re-probe while unhealthy: + // a failed probe does not advance the throttle window. + pt.Run(context.Background(), prober, prober, false) + pt.Run(context.Background(), prober, prober, false) + pt.Run(context.Background(), prober, prober, false) + + if probes != 3 { + t.Fatalf("expected every non-overlapping caller to probe while unhealthy, got %d", probes) + } +} + +func TestProbeThrottle_SingleFlightSharesResult(t *testing.T) { + pt := newProbeThrottle(time.Minute) + + var probes int32 + release := make(chan struct{}) + started := make(chan struct{}) + + // First caller blocks inside the probe until released, holding the lock so + // the others pile up behind it. + prober := fakeProber{onProbe: func() bool { + if atomic.AddInt32(&probes, 1) == 1 { + close(started) + <-release + } + return false // unhealthy — the share must happen regardless of result + }} + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + pt.Run(context.Background(), prober, prober, false) + }() + + <-started // ensure the first probe is in flight before the burst arrives + + const waiters = 9 + wg.Add(waiters) + for i := 0; i < waiters; i++ { + go func() { + defer wg.Done() + pt.Run(context.Background(), prober, prober, false) + }() + } + + // Give the waiters time to block on the lock, then let the first finish. + time.Sleep(50 * time.Millisecond) + close(release) + wg.Wait() + + if got := atomic.LoadInt32(&probes); got != 1 { + t.Fatalf("expected a concurrent burst to run exactly 1 probe, got %d", got) + } +} diff --git a/client/server/server.go b/client/server/server.go index a4d53a823..ea9f41951 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -19,6 +19,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" gstatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/expose" @@ -67,7 +68,19 @@ type Server struct { logFile string + // uiLogPath is the desktop UI's absolute log path, reported via + // RegisterUILog. Guarded by mutex. Consumed by DebugBundle so the bundle + // can collect the GUI log even though the daemon runs as root and can't + // resolve the user's config dir. Last-writer-wins (one UI per socket). + uiLogPath string + oauthAuthFlow oauthAuthFlow + // extendAuthSessionFlow holds the pending PKCE flow created by + // RequestExtendAuthSession until WaitExtendAuthSession resolves it. + // Kept separate from oauthAuthFlow (which is reserved for the SSH + // JWT path) so a concurrent SSH auth doesn't clobber the session + // extend flow or vice versa. + extendAuthSessionFlow *auth.PendingFlow mutex sync.Mutex config *profilemanager.Config @@ -87,7 +100,7 @@ type Server struct { statusRecorder *peer.Status sessionWatcher *internal.SessionWatcher - lastProbe time.Time + probeThrottle *probeThrottle persistSyncResponse bool isSessionActive atomic.Bool @@ -135,6 +148,8 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable captureEnabled: captureEnabled, networksDisabled: networksDisabled, jwtCache: newJWTCache(), + extendAuthSessionFlow: auth.NewPendingFlow(), + probeThrottle: newProbeThrottle(probeThreshold), } agent := &serverAgent{s} s.sleepHandler = sleephandler.New(agent) @@ -152,6 +167,15 @@ func (s *Server) Start() error { } state := internal.CtxGetState(s.rootCtx) + // Every contextState.Set in the connect/login/server paths must push a + // SubscribeStatus snapshot, otherwise transitions that don't happen to + // be accompanied by a Mark{Management,Signal,...} call (e.g. plain + // StatusNeedsLogin after a PermissionDenied login, StatusLoginFailed + // after OAuth init failure, StatusIdle in the Login defer) leave the + // UI stuck on the previous status until the next unrelated peer event. + // Binding the recorder here means new state.Set callsites don't have + // to opt in individually. + state.SetOnChange(s.statusRecorder.NotifyStateChange) if err := handlePanicLog(); err != nil { log.Warnf("failed to redirect stderr: %v", err) @@ -236,7 +260,7 @@ func (s *Server) Start() error { s.clientRunningChan = make(chan struct{}) s.clientGiveUpChan = make(chan struct{}) go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.clientRunningChan, s.clientGiveUpChan) - s.publishConfigChangedEvent("startup") + s.publishConfigChangedEvent(proto.MetadataSourceStartup) return nil } @@ -253,6 +277,10 @@ func (s *Server) Start() error { // "intent" (clientRunning) is maintained by the RPC handlers, not by this // goroutine. func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profilemanager.Config, statusRecorder *peer.Status, runningChan chan struct{}, giveUpChan chan struct{}) { + // close(giveUpChan) MUST run on every exit path (DisableAutoConnect + // return, backoff.Retry return, panic) — Down() blocks for up to 5s + // waiting on this signal before flipping the state to Idle, and a + // missed close leaves Down() always hitting the timeout. defer func() { if giveUpChan != nil { close(giveUpChan) @@ -291,6 +319,15 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profil runOperation := func() error { err := s.connect(ctx, profileConfig, statusRecorder, runningChan) if err != nil { + // PermissionDenied means the daemon transitioned to NeedsLogin + // inside connect(). Without backoff.Permanent the outer retry + // re-enters connect(), which resets the state to Connecting and + // makes the tray flicker between NeedsLogin and Connecting until + // the user logs in. Stop retrying and let the state stick. + if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied { + log.Debugf("run client connection exited with PermissionDenied, waiting for login") + return backoff.Permanent(err) + } log.Debugf("run client connection exited with error: %v. Will retry in the background", err) return err } @@ -385,6 +422,16 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques return nil, fmt.Errorf("failed to update profile config: %w", err) } + // Apply the lazy connection toggle to the running engine so it takes + // effect without a down/up. s.mutex is already held. + if msg.LazyConnectionEnabled != nil && s.connectClient != nil { + if engine := s.connectClient.Engine(); engine != nil { + if err := engine.SetLazyConnEnabled(msg.GetLazyConnectionEnabled()); err != nil { + log.Errorf("failed to apply lazy connection change at runtime: %v", err) + } + } + } + return &proto.SetConfigResponse{}, nil } @@ -425,7 +472,7 @@ func (s *Server) setConfigInputFromRequest(msg *proto.SetConfigRequest) (profile wgPort := int(*msg.WireguardPort) config.WireguardPort = &wgPort } - if msg.OptionalPreSharedKey != nil && *msg.OptionalPreSharedKey != "" { + if msg.OptionalPreSharedKey != nil { config.PreSharedKey = msg.OptionalPreSharedKey } @@ -577,8 +624,6 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro return &proto.LoginResponse{}, nil } - state.Set(internal.StatusConnecting) - if msg.SetupKey == "" { hint := "" if msg.Hint != nil { @@ -593,6 +638,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro if s.oauthAuthFlow.flow != nil && s.oauthAuthFlow.flow.GetClientID(ctx) == oAuthFlow.GetClientID(ctx) { if s.oauthAuthFlow.expiresAt.After(time.Now().Add(90 * time.Second)) { log.Debugf("using previous oauth flow info") + state.Set(internal.StatusNeedsLogin) return &proto.LoginResponse{ NeedsSSOLogin: true, VerificationURI: s.oauthAuthFlow.info.VerificationURI, @@ -629,6 +675,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro }, nil } + // Setup-key path: we are about to dial Management with the key, so the + // Connecting paint is meaningful here — unlike the SSO branch above, + // which returns NeedsLogin and parks on the browser leg. + state.Set(internal.StatusConnecting) + if loginStatus, err := s.loginAttempt(ctx, msg.SetupKey, ""); err != nil { state.Set(loginStatus) return nil, err @@ -637,8 +688,43 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro return &proto.LoginResponse{}, nil } -// WaitSSOLogin uses the userCode to validate the TokenInfo and -// waits for the user to continue with the login on a browser +// WaitSSOLogin validates the supplied userCode against the in-flight OAuth +// device/PKCE flow and blocks until the user finishes the browser leg. +// +// The daemon holds StatusNeedsLogin for the whole browser wait (set on +// entry): the login is not done until the token returns, so a client that +// (re)attaches mid-wait — a restarted UI, a second `netbird up` — reads +// "login required" and offers the affordance, instead of a Connecting that +// never resolves. The wait is also tied to the caller's context (see the +// goroutine below), so a client that goes away cancels the wait instead of +// orphaning it on rootCtx until the device-code window expires. +// +// State transitions on exit: +// +// ┌──────────────────────────────────────────┬──────────────────────────────────┐ +// │ Outcome │ contextState │ +// ├──────────────────────────────────────────┼──────────────────────────────────┤ +// │ Success → loginAttempt ok │ NeedsLogin held; the caller's Up │ +// │ │ drives Connecting → Connected │ +// │ Success → loginAttempt → still-NeedsLogin│ StatusNeedsLogin (loginAttempt) │ +// │ Success → loginAttempt error │ StatusLoginFailed (loginAttempt) │ +// │ UserCode mismatch │ StatusLoginFailed │ +// │ WaitToken: context.Canceled │ NeedsLogin held. Caller gone │ +// │ (caller went away — UI restart / │ (UI/CLI) → a fresh client │ +// │ Ctrl+C — or internal abort: profile │ shows the login affordance; │ +// │ switch / app quit / another │ internal aborts are │ +// │ WaitSSOLogin via actCancel/waitCancel) │ overwritten by the next Up. │ +// │ WaitToken: context.DeadlineExceeded │ StatusNeedsLogin │ +// │ (OAuth device-code window expired │ (retryable; the UI's "Connect" │ +// │ while waiting on the browser leg) │ re-enters the Login flow) │ +// │ WaitToken: any other error │ StatusLoginFailed │ +// │ (access_denied, expired_token, HTTP │ (genuine auth/IO failure; │ +// │ failure, token validation rejection) │ surfaced verbatim to caller) │ +// └──────────────────────────────────────────┴──────────────────────────────────┘ +// +// The defer still applies a StatusIdle fallback for the early +// oauth-flow-not-initialized return (before the entry Set), so a half state +// doesn't leak when there is nothing to wait on. func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLoginRequest) (*proto.WaitSSOLoginResponse, error) { s.mutex.Lock() if s.actCancel != nil { @@ -646,6 +732,21 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin } ctx, cancel := context.WithCancel(s.rootCtx) + // Tie the in-flight browser wait to the caller. ctx stays rooted in + // rootCtx so CtxGetState resolves the daemon's contextState, but if the + // UI window or CLI that drove the login goes away mid-flow (restart, + // Ctrl+C) the gRPC callerCtx cancels and we cancel the wait instead of + // orphaning it on rootCtx until the OAuth device-code window expires. + // The goroutine exits as soon as either context completes, so it can't + // outlive the RPC. + go func() { + select { + case <-callerCtx.Done(): + cancel() + case <-ctx.Done(): + } + }() + md, ok := metadata.FromIncomingContext(callerCtx) if ok { ctx = metadata.NewOutgoingContext(ctx, md) @@ -671,7 +772,11 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin } }() - state.Set(internal.StatusConnecting) + // Hold NeedsLogin for the whole browser wait — the login is not done + // until the token returns, so a client that (re)attaches mid-wait + // (restarted UI, second `netbird up`) reads "login required" and offers + // the affordance instead of a Connecting that never resolves. + state.Set(internal.StatusNeedsLogin) s.mutex.Lock() flowInfo := s.oauthAuthFlow.info @@ -698,7 +803,30 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin s.mutex.Lock() s.oauthAuthFlow.expiresAt = time.Now() s.mutex.Unlock() - state.Set(internal.StatusLoginFailed) + switch { + case errors.Is(err, context.Canceled): + // External abort. If our caller cancelled (the client closed + // the browser-login popup, or the UI went away — callerCtx is + // done), clear the abandoned OAuth flow so a fresh Login starts + // a new device code instead of reusing this one. The entry + // NeedsLogin stays in place, so a reattaching client shows the + // login affordance. An internal abort (actCancel from a new + // Login/WaitSSOLogin, callerCtx still live) leaves the flow for + // the new owner — don't clobber it. + if callerCtx.Err() != nil { + s.mutex.Lock() + s.oauthAuthFlow = oauthAuthFlow{} + s.mutex.Unlock() + } + case errors.Is(err, context.DeadlineExceeded): + // OAuth device-code window expired with no user action. + // Retryable — leave the daemon in NeedsLogin so the UI + // keeps the Login affordance instead of reading as a + // hard failure. + state.Set(internal.StatusNeedsLogin) + default: + state.Set(internal.StatusLoginFailed) + } log.Errorf("waiting for browser login failed: %v", err) return nil, err } @@ -755,6 +883,22 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR return nil, err } + // StatusNeedsLogin is a legitimate fresh-start entry state: a successful + // WaitSSOLogin deliberately leaves the daemon in NeedsLogin (the login is + // done, the token is in hand, but the engine hasn't been brought up yet — + // see WaitSSOLogin's state-transition table). The same holds after a + // mid-session expiry tore the engine down (clientRunning == false) and the + // user re-authenticated. In both cases the caller's Up is expected to drive + // the connection; treat NeedsLogin like Idle and reset to Idle so the + // engine's own StatusConnecting → StatusConnected progression starts from a + // clean slate. Without this, the first Up after an SSO login fails with + // "up already in progress" and the user has to trigger Up a second time + // (CLI: re-run `netbird up`; GUI: click Connect again). + if status == internal.StatusNeedsLogin { + status = internal.StatusIdle + state.Set(internal.StatusIdle) + } + if status != internal.StatusIdle { s.mutex.Unlock() return nil, fmt.Errorf("up already in progress: current status %s", status) @@ -817,9 +961,12 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR s.clientGiveUpChan = make(chan struct{}) go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, s.clientRunningChan, s.clientGiveUpChan) - s.publishConfigChangedEvent("up_rpc") + s.publishConfigChangedEvent(proto.MetadataSourceUpRPC) s.mutex.Unlock() + if msg.GetAsync() { + return &proto.UpResponse{}, nil + } return s.waitForUp(callerCtx) } @@ -929,6 +1076,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{Id: activeProf.ID.String()}, nil } @@ -945,23 +1096,37 @@ func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownRes return nil, err } - state := internal.CtxGetState(s.rootCtx) - state.Set(internal.StatusIdle) - s.mutex.Unlock() // Wait for the connectWithRetryRuns goroutine to finish with a short timeout. // This prevents the goroutine from setting ErrResetConnection after Down() returns. - // The giveUpChan is closed at the end of connectWithRetryRuns. + // The giveUpChan is closed by the goroutine's deferred cleanup (see + // connectWithRetryRuns) on every exit path. A timeout here typically + // means the goroutine is still wedged inside a slow teardown step. if giveUpChan != nil { select { case <-giveUpChan: - log.Debugf("client goroutine finished successfully") + log.Debugf("client goroutine finished, giveUpChan closed") case <-time.After(5 * time.Second): log.Warnf("timeout waiting for client goroutine to finish, proceeding anyway") } } + // Set Idle only after the retry goroutine has exited (or timed out). + // Setting it earlier races with the goroutine's own Set(StatusConnecting) + // at the top of each retry attempt, which would leave the snapshot + // stuck at Connecting long after the user asked to disconnect. + internal.CtxGetState(s.rootCtx).Set(internal.StatusIdle) + + // Clear stale management/signal errors so the next Up() (typically for a + // different profile) starts with a clean status snapshot. Without this, + // a managementError left over from a LoginFailed cycle persists in the + // statusRecorder and appears in the new profile's initial + // SubscribeStatus snapshot, making the new profile look like it also + // failed to log in. + s.statusRecorder.MarkManagementDisconnected(nil) + s.statusRecorder.MarkSignalDisconnected(nil) + return &proto.DownResponse{}, nil } @@ -1172,7 +1337,19 @@ func (s *Server) sendLogoutRequestWithConfig(ctx context.Context, config *profil } }() - return mgmClient.Logout() + if err := mgmClient.Logout(); err != nil { + // The peer is already gone from the management server (e.g. deleted + // from the dashboard). The logout's goal — deregistering this peer — + // is therefore already satisfied, so treat NotFound as success rather + // than blocking the logout/profile-removal flow. + if logoutPeerGone(err) { + log.Infof("peer already removed from management server, treating logout as successful") + return nil + } + return err + } + + return nil } // Status returns the daemon status @@ -1225,9 +1402,24 @@ func (s *Server) Status( } } - status, err := internal.CtxGetState(s.rootCtx).Status() + return s.buildStatusResponse(ctx, msg) +} + +// buildStatusResponse composes a StatusResponse from the current daemon +// state. Shared between the unary Status RPC and the SubscribeStatus +// stream so both paths return identical snapshots. ctx scopes the health +// probe runProbes may trigger — a caller that disconnects cancels it. +func (s *Server) buildStatusResponse(ctx context.Context, msg *proto.StatusRequest) (*proto.StatusResponse, error) { + state := internal.CtxGetState(s.rootCtx) + status, err := state.Status() if err != nil { - return nil, err + // state.Status() blanks the status when err is set (e.g. management + // retry loop wrapped a connection error). The underlying status is + // still meaningful and the failure is already surfaced via + // FullStatus.ManagementState.Error, so don't propagate err — that + // would tear down the SubscribeStatus stream and cause the UI to + // mark the daemon as unreachable on every retry. + status = state.CurrentStatus() } if status == internal.StatusNeedsLogin && s.isSessionActive.Load() { @@ -1238,15 +1430,20 @@ func (s *Server) Status( statusResponse := proto.StatusResponse{Status: string(status), DaemonVersion: version.NetbirdVersion()} + if deadline := s.statusRecorder.GetSessionExpiresAt(); !deadline.IsZero() { + statusResponse.SessionExpiresAt = timestamppb.New(deadline) + } + s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) if msg.GetFullPeerStatus { - s.runProbes(msg.ShouldRunProbes) + s.runProbes(ctx, msg.ShouldRunProbes) fullStatus := s.statusRecorder.GetFullStatus() pbFullStatus := fullStatus.ToProto() pbFullStatus.Events = s.statusRecorder.GetEventHistory() pbFullStatus.SshServerState = s.getSSHServerState() + pbFullStatus.NetworksRevision = s.statusRecorder.GetNetworksRevision() statusResponse.FullStatus = pbFullStatus } @@ -1467,6 +1664,151 @@ func (s *Server) WaitJWTToken( }, nil } +// RequestExtendAuthSession initiates the SSO session-extension flow and +// returns the verification URI the UI should open. The flow state is held +// in s.extendAuthSessionFlow until WaitExtendAuthSession resolves it. +func (s *Server) RequestExtendAuthSession( + ctx context.Context, + msg *proto.RequestExtendAuthSessionRequest, +) (*proto.RequestExtendAuthSessionResponse, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + s.mutex.Lock() + config := s.config + connectClient := s.connectClient + s.mutex.Unlock() + + if config == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "client is not configured") + } + if connectClient == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "client is not running") + } + + hint := "" + if msg.Hint != nil { + hint = *msg.Hint + } + if hint == "" { + hint = profilemanager.GetLoginHint() + } + + isDesktop := isUnixRunningDesktop() + oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isDesktop, false, hint) + if err != nil { + return nil, gstatus.Errorf(codes.Internal, "failed to create OAuth flow: %v", err) + } + + authInfo, err := oAuthFlow.RequestAuthInfo(ctx) + if err != nil { + return nil, gstatus.Errorf(codes.Internal, "failed to request auth info: %v", err) + } + + s.extendAuthSessionFlow.Set(oAuthFlow, authInfo) + + return &proto.RequestExtendAuthSessionResponse{ + VerificationURI: authInfo.VerificationURI, + VerificationURIComplete: authInfo.VerificationURIComplete, + UserCode: authInfo.UserCode, + DeviceCode: authInfo.DeviceCode, + ExpiresIn: int64(authInfo.ExpiresIn), + }, nil +} + +// WaitExtendAuthSession blocks until the user completes the SSO step +// initiated by RequestExtendAuthSession, then forwards the resulting JWT +// to the management server's ExtendAuthSession RPC. The returned deadline +// is also applied locally via the engine so SubscribeStatus consumers see +// the refreshed state. +func (s *Server) WaitExtendAuthSession( + ctx context.Context, + req *proto.WaitExtendAuthSessionRequest, +) (*proto.WaitExtendAuthSessionResponse, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + oAuthFlow, authInfo, ok := s.extendAuthSessionFlow.Get() + + s.mutex.Lock() + connectClient := s.connectClient + s.mutex.Unlock() + + if !ok || authInfo.DeviceCode != req.DeviceCode { + return nil, gstatus.Errorf(codes.InvalidArgument, "invalid device code or no active extend-session flow") + } + + // Preempt a previous WaitExtendAuthSession (e.g. when the tray + // notification and the about-to-expire dialog both start a flow on + // the same deadline). The older waiter exits via context.Canceled; + // the new one takes over the IdP poll. + s.extendAuthSessionFlow.CancelWait() + + waitCtx, cancel := context.WithCancel(ctx) + defer cancel() + s.extendAuthSessionFlow.SetWaitCancel(cancel) + + tokenInfo, err := oAuthFlow.WaitToken(waitCtx, authInfo) + if err != nil { + if errors.Is(err, context.Canceled) { + return nil, gstatus.Errorf(codes.Canceled, "extend-session flow preempted") + } + return nil, gstatus.Errorf(codes.Internal, "failed to obtain JWT token: %v", err) + } + + // Clear pending flow before talking to mgm so a retry can re-initiate. + s.extendAuthSessionFlow.Clear() + + if connectClient == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "client is not running") + } + engine := connectClient.Engine() + if engine == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "engine is not initialised") + } + + deadline, err := engine.ExtendAuthSession(ctx, tokenInfo.GetTokenToUse()) + if err != nil { + // Log the full wrapped chain, but return only the innermost gRPC + // status (code + clean desc) so the UI shows the root cause, not + // the daemon's wrapping layers. + log.Errorf("management ExtendAuthSession failed: %v", err) + if st := innermostStatus(err); st != nil { + return nil, gstatus.Error(st.Code(), st.Message()) + } + return nil, gstatus.Errorf(codes.Internal, "%v", err) + } + + resp := &proto.WaitExtendAuthSessionResponse{} + if !deadline.IsZero() { + resp.SessionExpiresAt = timestamppb.New(deadline) + } + return resp, nil +} + +// DismissSessionWarning forwards the user's "Dismiss" click on the +// T-WarningLead notification down to the engine's sessionWatcher so the +// T-FinalWarningLead fallback is suppressed for the current deadline. +// Best-effort: when the client/engine is not yet running the call is a +// successful no-op (the watcher has no deadline to dismiss anyway). +func (s *Server) DismissSessionWarning( + _ context.Context, + _ *proto.DismissSessionWarningRequest, +) (*proto.DismissSessionWarningResponse, error) { + s.mutex.Lock() + connectClient := s.connectClient + s.mutex.Unlock() + if connectClient == nil { + return &proto.DismissSessionWarningResponse{}, nil + } + if engine := connectClient.Engine(); engine != nil { + engine.DismissSessionWarning() + } + return &proto.DismissSessionWarningResponse{}, nil +} + // ExposeService exposes a local port via the NetBird reverse proxy. func (s *Server) ExposeService(req *proto.ExposeServiceRequest, srv proto.DaemonService_ExposeServiceServer) error { s.mutex.Lock() @@ -1533,7 +1875,7 @@ func isUnixRunningDesktop() bool { return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != "" } -func (s *Server) runProbes(waitForProbeResult bool) { +func (s *Server) runProbes(ctx context.Context, waitForProbeResult bool) { if s.connectClient == nil { return } @@ -1543,15 +1885,7 @@ func (s *Server) runProbes(waitForProbeResult bool) { return } - if time.Since(s.lastProbe) > probeThreshold { - if engine.RunHealthProbes(waitForProbeResult) { - s.lastProbe = time.Now() - } - } else { - if err := s.statusRecorder.RefreshWireGuardStats(); err != nil { - log.Debugf("failed to refresh WireGuard stats: %v", err) - } - } + s.probeThrottle.Run(ctx, engine, s.statusRecorder, waitForProbeResult) } // GetConfig of the daemon. @@ -1681,6 +2015,8 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) ( return nil, fmt.Errorf("failed to create profile: %w", err) } + s.publishProfileListChanged(msg.ProfileName) + return &proto.AddProfileResponse{Id: created.ID.String()}, nil } @@ -1707,6 +2043,8 @@ func (s *Server) RenameProfile(ctx context.Context, msg *proto.RenameProfileRequ return nil, fmt.Errorf("failed to rename profile: %w", err) } + s.publishProfileListChanged(msg.NewProfileName) + return &proto.RenameProfileResponse{OldProfileName: resolved.Name}, nil } @@ -1737,9 +2075,51 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ return nil, fmt.Errorf("failed to remove profile: %w", err) } + s.publishProfileListChanged(msg.ProfileName) + return &proto.RemoveProfileResponse{Id: resolved.ID.String()}, nil } +// publishProfileListChanged nudges the desktop UI to refresh its profile list +// after a CLI-driven add/remove. The daemon exposes no dedicated +// profile-changed RPC event, and a profile add/remove doesn't move the +// connection status, so the UI's SubscribeStatus path never fires for it (and +// the tray's status-string guard would swallow it anyway). Instead we publish +// a marked INFO/SYSTEM event over SubscribeEvents: the UI's dispatchSystemEvent +// recognises the metadata "kind" marker and translates it into its internal +// profile-changed signal that both the tray menu and the React profile views +// already subscribe to (see proto.MetadataKindProfileListChanged, recognised in +// client/ui/services/daemon_feed.go). userMessage is intentionally empty so this +// stays a silent refresh signal rather than a user-facing notification. +func (s *Server) publishProfileListChanged(profileName string) { + s.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_SYSTEM, + "Profile list changed", + "", + map[string]string{proto.MetadataKindKey: proto.MetadataKindProfileListChanged, proto.MetadataProfileKey: profileName}, + ) +} + +// publishLogLevelChanged signals the desktop UI that the daemon log level +// changed, so it can attach/detach its rotated gui-client.log. Like +// publishProfileListChanged, this rides the SubscribeEvents stream as a marked +// INFO/SYSTEM event (kind "log-level-changed", level the lowercase logrus +// name); the UI's dispatchSystemEvent recognises the marker and routes it to +// the logging toggle instead of an OS toast (userMessage is empty so it stays +// a silent control signal). The "level" value matches log.Level.String() +// (e.g. "debug", "info") so the UI can parse it directly. See +// proto.MetadataKindLogLevelChanged, recognised in client/ui/services/daemon_feed.go. +func (s *Server) publishLogLevelChanged(level string) { + s.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_SYSTEM, + "Log level changed", + "", + map[string]string{proto.MetadataKindKey: proto.MetadataKindLogLevelChanged, proto.MetadataLevelKey: level}, + ) +} + // ListProfiles lists all profiles in the daemon. func (s *Server) ListProfiles(ctx context.Context, msg *proto.ListProfilesRequest) (*proto.ListProfilesResponse, error) { s.mutex.Lock() @@ -1811,11 +2191,33 @@ func (s *Server) GetFeatures(ctx context.Context, msg *proto.GetFeaturesRequest) DisableProfiles: s.checkProfilesDisabled(), DisableUpdateSettings: s.checkUpdateSettingsDisabled(), DisableNetworks: s.checkNetworksDisabled(), + DisableAdvancedView: s.checkDisableAdvancedView(), } return features, nil } +// WailsUIReady is a no-op the Wails UI probes at startup; merely answering it +// (rather than returning Unimplemented) tells the UI this daemon is new enough. +func (s *Server) WailsUIReady(context.Context, *proto.WailsUIReadyRequest) (*proto.WailsUIReadyResponse, error) { + return &proto.WailsUIReadyResponse{}, nil +} + +// checkDisableAdvancedView reports the MDM-policy directive for the +// upcoming UI's advanced-view section. Tristate: returns nil when no +// MDM directive is set so the UI applies its own default; returns +// &true / &false when MDM explicitly enforces. No CLI flag backs +// this feature — MDM is the sole source. +func (s *Server) checkDisableAdvancedView() *bool { + if s.config == nil { + return nil + } + if v, ok := s.config.Policy().GetBool(mdm.KeyDisableAdvancedView); ok { + return &v + } + return nil +} + func (s *Server) connect(ctx context.Context, config *profilemanager.Config, statusRecorder *peer.Status, runningChan chan struct{}) error { log.Tracef("running client connection") client := internal.NewConnectClient(ctx, config, statusRecorder) @@ -1985,3 +2387,28 @@ func persistLoginOverrides(activeProf *profilemanager.ActiveProfileState, manage } return nil } + +// logoutPeerGone reports whether a management Logout failed because the peer +// no longer exists server-side (gRPC NotFound), walking the wrap chain since +// the client wraps the gRPC status with fmt.Errorf. +func logoutPeerGone(err error) bool { + for e := err; e != nil; e = errors.Unwrap(e) { + if s, ok := gstatus.FromError(e); ok && s.Code() == codes.NotFound { + return true + } + } + return false +} + +// innermostStatus walks the wrap chain and returns the deepest gRPC status, +// or nil when none is present. gstatus.FromError does not unwrap, so a status +// wrapped with fmt.Errorf %w would otherwise be missed. +func innermostStatus(err error) *gstatus.Status { + var found *gstatus.Status + for e := err; e != nil; e = errors.Unwrap(e) { + if s, ok := gstatus.FromError(e); ok { + found = s + } + } + return found +} diff --git a/client/server/server_test.go b/client/server/server_test.go index fa9599818..6c3ce9b60 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -61,9 +61,25 @@ var ( } ) -// TestConnectWithRetryRuns checks that the connectWithRetry function runs and runs the retries according to the times specified via environment variables -// we will use a management server started via to simulate the server and capture the number of retries -func TestConnectWithRetryRuns(t *testing.T) { +// TestConnectStopsRetryOnPermissionDenied verifies connectWithRetryRuns stops after a single login +// attempt on PermissionDenied, despite the fast retry config that would otherwise drive several. +func TestConnectStopsRetryOnPermissionDenied(t *testing.T) { + // Redirect profile paths to a temp dir so the test does not need root. + tempDir := t.TempDir() + origDefaultProfileDir := profilemanager.DefaultConfigPathDir + origActiveProfileStatePath := profilemanager.ActiveProfileStatePath + origDefaultConfigPath := profilemanager.DefaultConfigPath + profilemanager.ConfigDirOverride = tempDir + profilemanager.DefaultConfigPathDir = tempDir + profilemanager.ActiveProfileStatePath = filepath.Join(tempDir, "active_profile.json") + profilemanager.DefaultConfigPath = filepath.Join(tempDir, "default.json") + t.Cleanup(func() { + profilemanager.DefaultConfigPathDir = origDefaultProfileDir + profilemanager.ActiveProfileStatePath = origActiveProfileStatePath + profilemanager.DefaultConfigPath = origDefaultConfigPath + profilemanager.ConfigDirOverride = "" + }) + // start the signal server _, signalAddr, err := startSignal(t) if err != nil { @@ -115,8 +131,8 @@ func TestConnectWithRetryRuns(t *testing.T) { t.Setenv(retryMultiplierVar, "1") s.connectWithRetryRuns(ctx, config, s.statusRecorder, nil, nil) - if counter < 3 { - t.Fatalf("expected counter > 2, got %d", counter) + if counter != 1 { + t.Fatalf("expected exactly 1 login attempt (PermissionDenied must stop the retry loop), got %d", counter) } } diff --git a/client/server/status_stream.go b/client/server/status_stream.go new file mode 100644 index 000000000..c6ba547eb --- /dev/null +++ b/client/server/status_stream.go @@ -0,0 +1,57 @@ +package server + +import ( + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/proto" +) + +// SubscribeStatus pushes a fresh StatusResponse on every connection state +// change. The first message is the current snapshot, so a re-subscribing +// client doesn't need to also call Status. Subsequent messages fire when +// the peer recorder reports any of: connected/disconnected/connecting, +// management or signal flip, address change, or peers list change. +// +// The change channel coalesces bursts to a single tick. If the consumer +// is slow the daemon drops extras (not blocks), and the next snapshot +// the consumer pulls already reflects everything. +func (s *Server) SubscribeStatus(req *proto.StatusRequest, stream proto.DaemonService_SubscribeStatusServer) error { + subID, ch := s.statusRecorder.SubscribeToStateChanges() + defer func() { + s.statusRecorder.UnsubscribeFromStateChanges(subID) + log.Debug("client unsubscribed from status updates") + }() + + log.Debug("client subscribed to status updates") + + if err := s.sendStatusSnapshot(req, stream); err != nil { + return err + } + + for { + select { + case _, ok := <-ch: + if !ok { + return nil + } + if err := s.sendStatusSnapshot(req, stream); err != nil { + return err + } + case <-stream.Context().Done(): + return nil + } + } +} + +func (s *Server) sendStatusSnapshot(req *proto.StatusRequest, stream proto.DaemonService_SubscribeStatusServer) error { + resp, err := s.buildStatusResponse(stream.Context(), req) + if err != nil { + log.Warnf("build status snapshot for stream: %v", err) + return err + } + if err := stream.Send(resp); err != nil { + log.Warnf("send status snapshot to stream: %v", err) + return err + } + return nil +} diff --git a/client/ssh/server/test.go b/client/ssh/server/test.go index 454d3afa3..e2be0551c 100644 --- a/client/ssh/server/test.go +++ b/client/ssh/server/test.go @@ -1,3 +1,11 @@ +// This file is intentionally named test.go (not test_test.go) so the exported +// StartTestServer helper is visible to the ssh/proxy and ssh/client external +// test packages, not just this package's own tests. The //go:build !js tag +// keeps its "testing" import — and the whole testing/flag/regexp transitive +// chain it drags in — out of the wasm client, which links ssh/server through +// the engine but never runs Go tests under GOOS=js. +//go:build !js + package server import ( diff --git a/client/status/status.go b/client/status/status.go index 5b815aaa3..a53585c99 100644 --- a/client/status/status.go +++ b/client/status/status.go @@ -55,6 +55,10 @@ type ConvertOptions struct { IPsFilter map[string]struct{} ConnectionTypeFilter string ProfileName string + // SessionExpiresAt is the absolute UTC instant at which the peer's SSO + // session expires. Zero when the peer is not SSO-tracked or login + // expiration is disabled. Sourced from StatusResponse.SessionExpiresAt. + SessionExpiresAt time.Time } type PeerStateDetailOutput struct { @@ -155,6 +159,11 @@ type OutputOverview struct { LazyConnectionEnabled bool `json:"lazyConnectionEnabled" yaml:"lazyConnectionEnabled"` ProfileName string `json:"profileName" yaml:"profileName"` SSHServerState SSHServerStateOutput `json:"sshServer" yaml:"sshServer"` + // SessionExpiresAt is the absolute UTC instant at which the peer's SSO + // session expires. nil when the peer is not SSO-tracked or login + // expiration is disabled. Pointer (rather than zero-value time.Time) so + // JSON / YAML omit the field entirely with `,omitempty`. + SessionExpiresAt *time.Time `json:"sessionExpiresAt,omitempty" yaml:"sessionExpiresAt,omitempty"` } // ConvertToStatusOutputOverview converts protobuf status to the output overview. @@ -201,6 +210,10 @@ func ConvertToStatusOutputOverview(pbFullStatus *proto.FullStatus, opts ConvertO ProfileName: opts.ProfileName, SSHServerState: sshServerOverview, } + if !opts.SessionExpiresAt.IsZero() { + t := opts.SessionExpiresAt + overview.SessionExpiresAt = &t + } if opts.Anonymize { anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) @@ -547,6 +560,15 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS peersCountString := fmt.Sprintf("%d/%d Connected", o.Peers.Connected, o.Peers.Total) + var sessionExpiryString string + if o.SessionExpiresAt != nil && !o.SessionExpiresAt.IsZero() { + sessionExpiryString = fmt.Sprintf( + "Session expires: %s (in %s)\n", + o.SessionExpiresAt.Format(time.RFC3339), + FormatRemainingDuration(time.Until(*o.SessionExpiresAt)), + ) + } + var forwardingRulesString string if o.NumberOfForwardingRules > 0 { forwardingRulesString = fmt.Sprintf("Forwarding rules: %d\n", o.NumberOfForwardingRules) @@ -593,6 +615,7 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS "SSH Server: %s\n"+ "Networks: %s\n"+ "%s"+ + "%s"+ "Peers count: %s\n", fmt.Sprintf("%s/%s%s", goos, goarch, goarm), daemonVersion, @@ -612,6 +635,7 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS sshServerStatus, networks, forwardingRulesString, + sessionExpiryString, peersCountString, ) return summary @@ -1025,3 +1049,57 @@ func anonymizeOverview(a *anonymize.Anonymizer, overview *OutputOverview) { overview.SSHServerState.Sessions[i].Command = a.AnonymizeString(session.Command) } } + +// FormatRemainingDuration renders a time.Duration for the "Session expires" +// line. Examples: "2h 15m", "47m 12s", "8s", "expired 3m ago". +// +// Granularity drops to seconds only under a minute, otherwise minutes are +// the smallest unit shown — sub-minute precision is noise for a deadline +// that's hours or days out. +func FormatRemainingDuration(d time.Duration) string { + if d <= 0 { + return "expired " + HumaniseDuration(-d) + " ago" + } + return HumaniseDuration(d) +} + +// HumaniseDuration renders a positive duration in compact form (e.g. +// "2h 15m", "47m", "8s"). Exposed alongside FormatRemainingDuration so +// callers that don't need the "expired … ago" wording can format +// positive durations directly. +func HumaniseDuration(d time.Duration) string { + if d < time.Minute { + s := int(d.Round(time.Second).Seconds()) + if s < 1 { + s = 1 + } + return fmt.Sprintf("%ds", s) + } + + const ( + day = 24 * time.Hour + hour = time.Hour + minute = time.Minute + ) + + days := int64(d / day) + d -= time.Duration(days) * day + hours := int64(d / hour) + d -= time.Duration(hours) * hour + minutes := int64(d / minute) + + switch { + case days > 0: + if hours == 0 { + return fmt.Sprintf("%dd", days) + } + return fmt.Sprintf("%dd %dh", days, hours) + case hours > 0: + if minutes == 0 { + return fmt.Sprintf("%dh", hours) + } + return fmt.Sprintf("%dh %dm", hours, minutes) + default: + return fmt.Sprintf("%dm", minutes) + } +} diff --git a/client/status/status_test.go b/client/status/status_test.go index 44fc30baf..2babd9342 100644 --- a/client/status/status_test.go +++ b/client/status/status_test.go @@ -648,6 +648,53 @@ func TestTimeAgo(t *testing.T) { } } +func TestHumaniseDuration(t *testing.T) { + cases := []struct { + in time.Duration + want string + }{ + {0, "1s"}, + {500 * time.Millisecond, "1s"}, + {8 * time.Second, "8s"}, + {59 * time.Second, "59s"}, + {time.Minute, "1m"}, + {47*time.Minute + 12*time.Second, "47m"}, + {time.Hour, "1h"}, + {2*time.Hour + 15*time.Minute, "2h 15m"}, + {2 * time.Hour, "2h"}, + {24 * time.Hour, "1d"}, + {2*24*time.Hour + 3*time.Hour, "2d 3h"}, + } + for _, tc := range cases { + got := HumaniseDuration(tc.in) + assert.Equal(t, tc.want, got, "input %s", tc.in) + } +} + +func TestFormatRemainingDuration_Expired(t *testing.T) { + assert.Equal(t, "expired 3m ago", FormatRemainingDuration(-3*time.Minute)) + assert.Equal(t, "expired 1s ago", FormatRemainingDuration(-500*time.Millisecond)) +} + +func TestSessionExpiresLineRendered(t *testing.T) { + in := overview // copy of the package-level fixture + deadline := time.Now().Add(2*time.Hour + 30*time.Minute).UTC() + in.SessionExpiresAt = &deadline + + out := in.GeneralSummary(false, false, false, false) + assert.Contains(t, out, "Session expires: ") + assert.Contains(t, out, deadline.Format(time.RFC3339)) + // 2h 30m drifts to "2h 29m" within 60s — match the family prefix. + assert.Contains(t, out, "(in 2h ") +} + +func TestSessionExpiresLineOmittedWhenNil(t *testing.T) { + in := overview + in.SessionExpiresAt = nil + out := in.GeneralSummary(false, false, false, false) + assert.NotContains(t, out, "Session expires") +} + func TestMapRelaysTransport(t *testing.T) { out := mapRelays([]*proto.RelayState{ {URI: "rels://relay.example:443", Available: true, Transport: "quic"}, diff --git a/client/ui/.gitignore b/client/ui/.gitignore new file mode 100644 index 000000000..9f233d8b6 --- /dev/null +++ b/client/ui/.gitignore @@ -0,0 +1,8 @@ +.task +bin +frontend/dist +frontend/node_modules +frontend/bindings +frontend/.vite +build/linux/appimage/build +build/windows/nsis/MicrosoftEdgeWebview2Setup.exe diff --git a/client/ui/Netbird.icns b/client/ui/Netbird.icns deleted file mode 100644 index 20af72825abc8eabdaa289fe0d2cd96b81217e41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80549 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 z 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 V>cxt`kmog7Ub4Mdd%@$u{{j=u(k1`^ diff --git a/client/ui/assets/netbird-disconnected.ico b/client/ui/assets/netbird-disconnected.ico deleted file mode 100644 index 812e9d283d096d823824fd66e6533359fb375b4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5056 zcmbtYi$7EU|39ci7|A8M&h;ZCN^PPP$}QB~%Kd&X z5v|!rNJVU@G}rMvKEFTVd-m94XRmY4>+*cP-p|+TeEE+o=GrUFHx3^L5htA5B=438qg#UQWPc{T#JKG-w#US$_RqN6XAC0Rg`8Vf zE84eI+8+kT?CY(*w6|8$v_58qt6?@yt`rRp4o+W0p+KAe`!VQB69F>lMHYEDU8EmB z7N@tePkbIZ{1AEdzEZ7BL{up;Ey@4@PDO64l+0{y6W^~71xI8AOk)V?GiS9KG!e-4 zwjjCp;E}0K3Bys`GmDPHa?G@xr-)fPk3C+p&GpsEgf&dW(Pj0RYnxJ7DEWCOedBaWq|c2bvwe+p6o9FF)4nvq zKCNMCvD4f>DK8AcQqF$g!Jl1wAen&vLg$#`0rKDmcRS+plHAfvew8*DY=&-Q;H)N! z4SG0urfykX;0$2Y6*6H4x(9;Ks%gaw*zCx-1Ft@;OnIe|`oPh#0CB;+ny;V~3-~IH}MFv~PgfWlpAHKd3FhEOs=&?6y z^0-#;tnI|q6urN_mT+&yl>D>p2u$}RJWgmfQsv3~#@#hj+(((pdoP;8@yS@CpHW>~ z`vH8ezSTEIE!*kgnIm_5p4^7a%+2*m32y~YpG*Iv_ZFA}Z5IsBrS;65dcS@G#l5JP z`jmP-gb8F4hjIl#-SDDQ6$f*-gYzvU%9 zE{yj>S@mQ=BSiyw1Mzw%3w2#vqqyV76CBh2BS8AyyLFyID?$^si8xve6t^Fr+bP!628V)ipPv2q0T+VR#CGb|F{A1Rhg^*|SA@Oz@>iB_**o% zpfBiko{N!-aLX`zq(pGi2$T3QJUpz0YNBvspB-^8bAf?K;{;q-MQJlxsI>6v|*J{;CB_i|a?cTp$9zi!hUZspw%`F+PgVm}^< z8h-a~@8c&=M!LH8B)UmQTw8u+o!()a6Pv+Ll@Ppu)6|Sw{tK!ow>RY|yYUiP?hz52 zc^+FVTZy)>7pDqX1epBN)>h==LcwsdxrIgFY81mXkxF7F)^P7x55ZWW4V(Dn3yc=Q z-l0{l?X_rhQt+)?#R$k2lz;Z0u@Q}Vrn8?=uKJGdk6`aM;CP(2x>jV`1w+;T%}VPTNxcY28R zZ{NzSc$o)C;)|ZDa>Z*mUc4T?AB%*I-V6z`>mNFZ|M3006e2-Nx&cJ-r(rx6UhJFQ zPH+ZePU^s(=0}JpNH;C3mt}ZCIpLajgSG{$V56ucw;#0x7hUAklh~_acMN7OJP2_7 z+@Y&vsduVixOfD*KO`1^#P$~Fr0n#tYkb?(Yb1}2r=%>6P2qD+d}}+xF_qo=HTBEV zL{K=h_~n1*;9YHLXrgs`b~_Y6y+HJR+tdQxXAC?Y_SU0NC^X8m)aWsb*hjKb0CIvC zGPu9k#V)Z$HFB~jHC2EyVCnJz1agE2K=G#7me02n#MIT*;XFZbtgOP;JK08f*l|H& z83H=Vr0%P|y?yb7|A0%9PoeO(=YvO(i`WCwc`05yPQ9>2DK52G5=zBsXs}ee>uK86 z*WBy4G@aDt^pz@YD}B*c+dAe%m+iB!KZ8B>QkaIE-}UAmw87lJBJfqf{l%+fs1F z8SMk~;5<%0M>(dZv*89}wWOBJrld`26p*ZFCtWVv9oeUN3L$2a4brtTgbcSyJXCUH zkRqsyq`paXECHp~5b~TF-0`8^cn31XbDp8SE_(9*ZI*|y@w|VFewF~BN0kje{&8r# z>a>D_F&rBqs=sV2zxB>q89=&~THcc(wWwa?4^2~&0y!>g@9yoY-#44BK(RqV=3*UU zwXUvCd%&er+=LGhAKZ@_S&iQD_!$|W(Vy7j)XxXxhx5fQEcng{#O%wyc*1#6EK3sv z@)QqXr5Ue-PI5j5=-1cRkCbEP^sQEOIpQNDC&04V6FV*_ti)Y>qnd!Mzru5LCA{ew zOTX~>_d_pnX@^ui>If(_vHI;>oRZFO^~fCNy@4~j!?B7H*NT@tmc632F0Qb5pZ z{{xL$dk3A^@{iex19kyI8^tq>Jfn}|uJ>dLy8ffK;!DgjYlN1YQ3Kf1%T>rSHoPfm z1`>p1V5;NM^pc*oJ4u=~*nZHV-2Oj@-spUvdpf{0NFkd%T z4{Gz{RjjG1`lCBW>css!SwZV_7cW2;f19*+=?1fS#*06#hv+fxge!Y<=mWipO|eY{ zbP8RNA8!5oz@&R7&|}HNWIDi}!~+apL9R(h9F^WW2u=wmDY1As5gev`4g$%^^%IND zgC+PEX^r;Adz{F0Z&j_{?Oc!ID_|strNyP-u62Z8veca7cNs%3eMG@0@}< zuoFZ_rvsL;JgdCukd_^dUcfU;$qw$dGcWu`y>)Fg1_8l5>n*{1Z`uVgO4`z1AGing4;(AB5Xw1;Kde~O(a~`;?R8yhvko-PSd?*_hSit?2G#sW&uFd07<^^WDBRyh)jN!&zVxV5{%R;b@cf00Pe4zjbwc=wp*%8lJR>jfSPCrvZ|{tfw> zJK%yCq&mRe(`0rAdrL8Zd|SxG74h)?b0=YtzZytjgUMtPJSxo&!U5B%+?L1dlW?At zw+g^0SYHW1#z%1UGrirXn;Ya)$LvI#$jK(>&nfIw41nz!3Ea4TrA{K@bJNlpX6-d1 z5vr>Lxh>e5nm-rMI#a?6!TfXf?~=w!J&9S}+_?lb-q$A8q+pftLgk7Io zy4L(@HLAX`F{|mD^XBwF$CE#BB9O}DH2HxIi}6a5TF%PE&B;EO8|8^^M(HyenN31= zAild(9MMm1U{y88%%{egMsc~+yhQgObT|YDL z*hb5d`S({hiy#2>LMf3Mx-(Yh@PrjoEGVjj2xKlz(6srobwbd@FRPO=cdK@I;DF3G zGw#wypBiru4}FfI^0C=QP6YR(&f)8^Gcz;gih?Z%xj_o2Pb+G@l)Pim5y9O!GafC2 zfjVLYJ4Q(@^5bkZhMI|@hqtPj5!VI}P$j;w=`stm0e*fMxbPtD<+ei0)(E9Z=n4kV zz%Ga@`5UB`J6SuTm0bd$*rvtz$O*Zlz)4nq{X6Gki?s)0ag?R#boR19Z@1fCO z!#(-8`T}zg{JTU*8|MQUAe=~;f52m*IDpg=HySQCP9rwEgL#%rcnWqhhG}MdVD9z; zy;760pr}t$0q7y;t%TVFE{4Z^eSK?kWjoP-j%kJ``0ntGsX>!%cFY(Hy=4>r2T)@^ zrO58cgOv|SI%CY}Yy8+b7Aq|dlhKql68OHUsY$b%i?AOs1>UcVFnp!kgP$55eSs*i zs1X0Fpv&gbVGx^3?f)skrzxea%Nm#SkQeH6a5dZ!zlL#8X8Ss&1pDY&O zCSD){-(OSCe#&-06+p=;yNIdfjQdH{$}fO0&d=S7rN+suZi-w}6&vs0=RPNrwo?a`L22A%*d57c;21=15}9?ktrkLheG# zynCP+AlDK?7Zw(*`BGd5DVMv2A;X*ji%;H41GGji7hJ%S$|s<&(}d4XnxkDDC;+e%G{H+S_Y0e2hGl|Fxq)#s;ORGgch^=Q)zZB=5dqa6?Dm zZxZ?lP8_)N=jFb6fxBuhhZ*3#DWk^}#0Xxf=E_c;HFtyRpPhX(Nl}{st0p2~4nCUx*N7L{Z1yuY-~ayB3e=i+|Lo;vZtUta M);HIyz`Mr%AJn`~GXMYp diff --git a/client/ui/assets/netbird-disconnected.png b/client/ui/assets/netbird-disconnected.png deleted file mode 100644 index 79d4775eab038831d38abcc2845f671adc7a1b4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7537 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 Qf&c&j07*qoM6N<$f}PFco&W#< literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-24.png b/client/ui/assets/netbird-menu-24.png new file mode 100644 index 0000000000000000000000000000000000000000..087c1c2aeef6cca5bf3793eee16d9b28acf758ed GIT binary patch literal 739 zcmV<90v!E`P)=$=#01N`yEPK_TG}%M%Uh+ocbdHf3UM#MBP_#-U1h8N-&mD!@!NAz~(@A zi*mxFqD_;sC1zn~v>{W7SqlgICNf*NT)OiLs3)qj`?b*KE`ajWU1vpP3}`L`7J(Th z^3rnL_eO_fR9&s~da0=)03{;i(p`51(v^E4_|l41o5yZl1yv^AA~W7$=LFX$0{EGt zcxrMGcuQkuU$R=Z<7aO;~vvGojL$Jq$ z5?&?leH`C;w_^;ExSqTTJS&-gIf(?j*DmV`^lJUit3WApYj5vvpZ#LIj`qvIiy~3M z!K^ey8N+;Q*x^DR*QBOrD+>VhjA~COFQMcOfL?4ujEWSHd<^kp04sT|B101Wr~2RK zywJIzzbEF+T5Tkx=wc&*Kq}ckx%TS<0DQh-=Z+wS-AS^O*=*g@@rpZ)bl_Ws z{w;C<<%T%zOWNS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5l&DCJ@J#ddWzYh$IT%If%U}(=|V1cUf1JWSi0mL9Z5Sn=bBUCj|YylHowa@}) z1RJDnrCM7Okm4-xh%9Dc;1&j9Muu5)Bp4W&EIeHtLo5W(2HR$cIEvKer+wR0-FY%I zNP<<+MZkKakZ1O4j*Y^XZrofVsArgXQEh5(>)NS|x;bk@ws2+q@nm9jc2H`n-+83s z-TgB*_ustLlfIlWy?ncQ{=Mq^=J)rRvv0W1^WXi#`EM%|>o-)&@Ko$#U^~?{qN%PR?;v zZYzDV@sB%ai^bOYOsdaf9Ar4|*ZWS{H^b!_>!v9#!bfg79a-+)$-1#q_g7;=tawB~ zrdLtV12ez9Kc6N2+`0B!kb82O>a{OgAF6_4+O`S!99jLjT;fEFU+DZK)0wdux8Brw z#w=i9TQ+^aK+1IC-0&K;#Zy>IBtpYPj=Xquho8l;^&!{$s<3HKcRxG(ly}0TptQ`; zGK;BGQ;(j?{S?gExwXG(=F#O}-{>h#|8ToI`}DyEV?8S#n=M zedo=+r=L$_YLK~ElRKM_h}%XV|4{ieeyzdp b>pz)Ul%Kp@p#5<>D1mvp`njxgN@xNArAI%^ literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-connected-16.png b/client/ui/assets/netbird-menu-dot-connected-16.png new file mode 100644 index 0000000000000000000000000000000000000000..3a7fa31a4031def909d9e51280e930d2cfa501d3 GIT binary patch literal 508 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`3dtTpz6=aiY77hwEes65fINBEI2;2C9rtu`46!)dH7!`Uw`D_l zW=8gu6$?3zZ)>}^lw;Fg#TT4RTiFg9uCEUkk?N>U|6}nyn(ssZ57y()eV>->FAJ{Y zVQ^UZYFlu=T=?6En*$7X>sG_x`?vobW*HZZUZhlH;S|t_@1`rL7Tk?SF88qNFlw{_n k7MCRE79jK(h8UVy8G($4=#f;Ez5~+a>FVdQ&MBb@0E!Z*0{{R3 literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-connected-22.png b/client/ui/assets/netbird-menu-dot-connected-22.png new file mode 100644 index 0000000000000000000000000000000000000000..78b068748396091b655aa0bdceaa1cd23fd612e7 GIT binary patch literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)63dtTpz6=aiY77hwEes65fI$sfT_}}{C)aurVgbUVw ziV`WR_Cl4W#5tg8YIRm^k^k6+O!)FsqbZ z`t|Q0i~a6DOvhDra{X1h2$Z@n$9n^)lrhQM-G!lpRn`N@;VkfoEC%{l6@(c*gH%2M z1=&kHeO=jKv5N9>aUJQH#tsyk;_2cTB5}F&VkBRSf&j~fGG-CR!uyQx>pdjXJB?oc zs!?$$XRHxA>M7*@!Dp7>DGAlcs>6F8ES=!x8Td)*f7a61$0S}S`vkv!wRY{hJFV9n zUzb!D+V430uV}}@`j;ovlk*LQWkr6DvalAcE-e$*gq-Y4UXSb6Mw<&;$URm(~pc literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-connected.png b/client/ui/assets/netbird-menu-dot-connected.png new file mode 100644 index 0000000000000000000000000000000000000000..fc8ce4d857b68466807de884b622b0d22ec2c6b2 GIT binary patch literal 452 zcmV;#0XzPQP)m1pa;b#dq-Q`>4`S0@rt4 zyL%0%4?r$&e0F;~!{2{I#SI2vX8KbvrRs_76L=~Dg*hl#$Tl1V{{CYDnE^5j!vau* z5*z0D0LToGSuj34h>Y)6Jf?;P4A4kFtK1>W0P_JTom0zj2B3+ literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-connecting-16.png b/client/ui/assets/netbird-menu-dot-connecting-16.png new file mode 100644 index 0000000000000000000000000000000000000000..f874706b5a357263a61b176ff373d688e8b164a0 GIT binary patch literal 520 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`3dtTpz6=aiY77hwEes65fINBEI2;2CUG{Ww46!(!oFKv4bA#pXx)mGP_W#Vt{rY{sF*7ss zVZ*dzd$V1C^uN2b^YiQljCZb^M)d8in|$>2pXE^>{?{9_v9Yy%@99}@_dhjrtB~l- zZ=s)$$)|kvdBNsW@$*{S^q>AqU;O`{Pxjk4FDj&iU|ZE$^->9%h|?|H_5_ z{EvSmKk0@&(42$+|Eq$;lrk7rG8}FYKGXC2p0Uxs1gGFH^Co^g%9(bomW5YCK-})r zPkry-*>1OofGORp23_C4D_>K!P%m;c;wZt`|BqgyV z)hf9t6-Y4{85o)A8kp)D8H5;`Ss9sG8JcPv7+4t?BwhR|jiMnpKP5A*5{m`{hz7?k vc|i3H8gLs*GILXlOA>Pn5PA$l3{9+zK*mG#NGeL-0crAd^>bP0l+XkKnYp#4 literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-connecting-22.png b/client/ui/assets/netbird-menu-dot-connecting-22.png new file mode 100644 index 0000000000000000000000000000000000000000..d8e5970f5605bfbd8a1080f7fbdfc78bce8ef0e6 GIT binary patch literal 637 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)63dtTpz6=aiY77hwEes65fI=ur6IsHAsMsIXOi_67NS zizNI0=P+DX*tbr+?3CcK6a1eK^M5$Zf0x1c-Wt`0DkY6wZ z6DJ?HqG#CzW|gu_zyAGWvETiN>A1>HuCl*M7nwXc4`pz^1i3TG+uensgH_f8$l)yT zh%5$rUKNBHJ%dy}00r4gJbhi+U$Khvad92#n8pqin(OJ}7$R}GbfUXZivbS{FI!F% zuUfjy&;P>WW}pK+vnarJN<6i+sxmICC{3q+y7m-tn)QA+tQyyS3CGw zThi+$vqN8Fv;E9kGV|(AXttRcutadma2#bk;FD$dAa-wUHBb-Y?4s9INRpm zzi&5R5Yo$tOF}f%p68DJ{Q^19Pw9`I|Chn0MoyCP$)oLjGgS^MPI$fg;1{AT=zeJ7 z((>oVd|U=&v#5qeBe7Ke#2$hc+zZtW5ifeQ{Lfzo7GeyC0TyP4KQH^_B9&HOUW3yD zkjt~4UEfZs;mDRulhX7=_6a-{fx;XVEaVyv0wBF0GeBlxSOAJpV#6FC0GRh*@0jS6Y*-WY7ATvN_ zVORhv&p|~lsK6rEaFAY*8OU`5&5LV{d;n4aDso8)E|6M~9(2R8`Tz!KT4kaGP{9Q% uTVUBAl!-v;{P*jP7l_eMQve760RR7Xh`~Y9fk(0c0000NBEI2;2C-S>2H46!(!oFKv4bAx5CxNZ{f?Oj)Iz4>3C&c?>p z7TnWQ9bQ>2e^*NU`|JgbYj&4!o)fh}q5J#W+gS(q*B_Mtf_uis`+w=^+{v0Y!Qk!o z)vx7KzMgr(cIN-r(|%9?{^nd%|F7e4&V!%d`+uK5KfkT*`ntLI3ckK#PWkHdf^Ej% z|L;|Pd|!XWUS(rF4-ZdM{eLfzSj#%O!wf-;ZH&t$B){)XPpdoFvh0Pe@#k)!gMM>J zO_(&P=*i{{1;4Y!riK5Pkd*vqXk@e}EwS=zWW)|zRn?~)R~HwG7yyIH>PTd3cQy+* z(5tE?t`Q|Ei6yC4$wjF^iowXh$V}J3RM*HL#L&#j$jr*nRNKJ7%D^D$;!kN54Y~O# znQ4_+G#EfMIBv-Us%OxE+fb63n_66wm|K9*V;Ev+Vr2v}9->E5QTh%@lc%en%Q~lo FCID9Cy}JMa literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-error-22.png b/client/ui/assets/netbird-menu-dot-error-22.png new file mode 100644 index 0000000000000000000000000000000000000000..d9bd013d66ec215d5926a805a2bed2c3bee2b026 GIT binary patch literal 629 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)63dtTpz6=aiY77hwEes65fI1Tq2zdAd=cXkHqdhg=$*~#gnvop}TiSJae11b5EAirP+CQd$X zMbEMc%qnGIh+L^k;On? ztAa42XOPMVpdfpRr>`sfD^^iHF0LaT)7XJRb39!fLnJPjUcAZItRTP|z@n?c^lta2 zfB((H*7|Nad*`?_)0vo-buKdmWEQ4Bdg3a%c)iNL#91F>JeDpwQ8}${tH$am>DO~i zcda}1XtD3@pBo+vO4nRBQ~%z)|AWm3=lT~XmOsej6qYZaC!YI0=c|$Omb^nd+ zwXEFYzim>I%ZwuXz#a#l7$zIe8UZPG3ArfI4G}BYR&4p-b$^NoHS@Z{5Ey z+66*-8F5LxTD6LY`O%{Sg#!oDAN}|tgH4Sb2gj2K4h}Q_%E?U-3k&-~v<1!o|GRYg z`p(B?FgAWn|n#(1IK1&xc5tl`a4C|36W|2LmjOjDMbXbwz3z8m_@<0m$VA zZf@I2H5}QJ3A=WABKrhx0VvFs7cII@Y_Q-0py0Y27pDn}6L>NOMJT0)g8;}3kXbN3 z+yYQ)q113}W`QjLrE_Yy9tJ>Wfy{(k0LnzvG8_hAW`Zq%n1JdYyog4?yKP$Q7gn7f3Bg4|-j|Fkn?CI-sP|r#S!w b00960)gi!0=-XYW00000NkvXXu0mjfUTLV+ literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-idle-16.png b/client/ui/assets/netbird-menu-dot-idle-16.png new file mode 100644 index 0000000000000000000000000000000000000000..354b5b860bfce07d9de3d22f59ba27cf77cd10e5 GIT binary patch literal 490 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`3dtTpz6=aiY77hwEes65fINBEI2;2CZS{0<46!(!oFKv4bAu(idfLWMA2&z-|MmIy%tj#S zZ7qGjynlaR*mRe_Y8ejS_w3X;G-poa|9=Hx#s&r*j;-;A8*B1wfB*fx<@^HVa@DsgLgQm@Ai)Sv;kp(HamwYVfPw*XC#5y*ImC6bELcYt~rJYD@<);T3K F0RRG?rY!&f literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-idle-22.png b/client/ui/assets/netbird-menu-dot-idle-22.png new file mode 100644 index 0000000000000000000000000000000000000000..675cf1ffecc5849c26f3f03d5a46500b8fa646f2 GIT binary patch literal 602 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)63dtTpz6=aiY77hwEes65fIP zpymV=oH~C4P%(q(r0U7Lfa>QgUO#8?2A~z$v5Uollw3)WUoZm`Cm*+>XW0a1m9k5} z{;}Ba{=;N{TxBQMU!{vooHvxj4**p%CV9KNFm$lWdH^|`1s;*bK#!_|Fr#OX$_Jnz zdx@v7EBh-}Q9drNBOTM&fkJ(rE{-7*mrE~3^0g=kuv`ce31Imid-3=Dpp-Nty_LUe zR2<3$YlT~SS{d&OAC2+QtdNN{@C;qn+kGx%)*lo7Q>Sj7J$h@E&ZquszPqcxuvg!{ zGH>_4^~=BCUD+(9|EeaW`ElmHm7gn>qIT46dd9u>P)@(ZMxn|1(|8Pbu-I_gbsS?n zyg0P+=pw0r%=caVw=9$$kAJRR26UNfiEBhjN@7W>RdP`(kYX@0Ff!9MFx53O3^6pZ zGBCC>HqtgQure^1sx)moiiX_$l+3hB+!~(L>v01$XuxeK$;?eHE=kNSK+|JrU}b0k VvBW2{))}aW!PC{xWt~$(695+S(e3~M literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-idle.png b/client/ui/assets/netbird-menu-dot-idle.png new file mode 100644 index 0000000000000000000000000000000000000000..79e7bbbf8cf94aaea503fc2ce2162a6b774ddbcf GIT binary patch literal 483 zcmV<90UZ8`P)QaLM8!3|&OXkUd_z^KlPc|HRcIM#h z=nI7OGUAf>KXodP^X@AJx#=ogKDhlsG~~#(xy%3l{YzBv z!ClJC{Kv*CIZ{>N^%|T$0J)s!PyBjP4F`e$|Nk>Rxf>AlN67LxvQOYX0EPJmC4=j@ z!kbtCikENBEI2;2Co%VEb46!)jC6&7I;J!10AXg!i=6lDj$G?>?NMQuI#T^ zMfte6j&w|82MSH_ba4!kxLkTMlCMQUfaO9Nv&hXm+r#lGhMP0%F`A6mE?~cb8I^v)~X}6 zno;}h{zt-EQ7Th~?llGg-KJXN8c~vxSdwa$T$Bo=7>o>z%ybP*b&U)|3{9*IjIE4K zwG9lc3=9HVS6HHG$jwj5OsmALLGgw_D^P<5+=i0O+|=Td#M}ZjJ%$EWh6WHzd@^gD QfqED`UHx3vIVCg!096^#6#xJL literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-menu-dot-offline.png b/client/ui/assets/netbird-menu-dot-offline.png new file mode 100644 index 0000000000000000000000000000000000000000..7aec5d01da5b9697291400ae9b341a6f3292102e GIT binary patch literal 456 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 diff --git a/client/ui/assets/netbird-systemtray-connected-macos.png b/client/ui/assets/netbird-systemtray-connected-macos.png index ead210250d7a4b25461aa0311fcb6c13cec75057..d29a7ade8821d87b12982d8b051ab5294a7a329d 100644 GIT binary patch literal 3690 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 OIuXWJMzz=7AN~`y+wr{s literal 3858 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 diff --git a/client/ui/assets/netbird-systemtray-connected-mono-dark.png b/client/ui/assets/netbird-systemtray-connected-mono-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1f7d40121c11e9886d395344ee8ec2ce96a6238d GIT binary patch literal 1548 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

6A!aevjJ&cU%ypida~ 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 diff --git a/client/ui/assets/netbird-systemtray-connecting-dark.ico b/client/ui/assets/netbird-systemtray-connecting-dark.ico deleted file mode 100644 index 615d40f075b41f3b3c9241611cc4b249d0851899..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105128 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 OF~=dJ2Ok^g7W{u+(t)V} diff --git a/client/ui/assets/netbird-systemtray-connecting-macos.png b/client/ui/assets/netbird-systemtray-connecting-macos.png index 0fe7fa0dbe50bcf8c08e45c7cf13b7e6e2348335..306c6ddf555e6049c57ce8bcf98eee80bd2db730 100644 GIT binary patch literal 3725 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 literal 3843 zcmb7Hi9ZwWAKxsKV`S@a4LQ1zww8BQ9L&hqTVnMm(VHeJ{&1|y{dKffC8 zlC}S(ws`iw%h`)dEHU_pdi6w_!N!#5PadL=QO=_N`v+iSQXXKHruP4hlr2#ImWi<% zoXzW?5qCCq9zWzAKF>etFjCUW=XJz{cY;N>4EbkxDG*Z)mIC{22W_B*_&~D~F>|Qs zfuCgL?asi2(Og$66y)<{L5gjPW2J8>|Lb96mYj=*R~wyiLKm`Q>^?GkDZx=zBsMz< zbs%^$vu6X95kUemyud|bem^J2p;jpI9KDUwfk+KV4!sD8{!p`Q*v~9*wYF@!=pY_~ zAr2Ez;#o{1kUxY<_qITrzOmuUr5U`{M+KMgcyPtro-FD#kME!@KROYJThh7^Y3l~^ z>Y1mAMg0b{{{u_&^{Gm&9vpb`dM`a12q+-u)pAxi}#xJhT=C5oEYp&EtDXkU@1YgYf8?DRRDT3IPSqFv} zdNn#Q60x4J)0xqYTf5n6r{%S~S1=tnVRzn|>o8qESav)wmjB4E=S#Yq_SnbS$%<}F zcBO=8wQ%YtPu|(K&xi4;=gmCeJwKI!*yJG=`@h7gnSl{XB*vJw;xUR-t?&a+iGRO3{#qA>%JU2A!L zJcly<{`*rRWE=KE8E2*PP_bVBFYlT9_|uSDbV}J;jjqiQmGnM}m7|RBO*X2Ngr1uW zB`57iAB>tDcg%oubmKB3k2{7iHY{@pXqG5=O@HDdO793TXK0W;|fg17hgDb z5xg)sz$6g(=MG&-GE(oT*l?tM44khezIS;X$xhQ`{%|nQavaP;k5K?MtN+FfL)=nb!f4e5ae%ZaR;@ zsofv|=}9vlZk&B*dIEy^PFSRyPGNh~5lfYI=nrG|Jh{AjeeGFRA2chIQcr(-a%%CF zDox}5klXZ*2Tz_JLv7*ACsI0{rwI@3lH)fIIw`{SRj$M8&#DH4*+V&rnJ&vAIw3cq zdL-+r!+x|Zy-B>FgkT5l$r+hQo2sAEpaS?(Iz!wf6qwm_^S|ibf)q9}&y;xC`{CpYm*VlNw`a&1Tt*Ix9CKJ5YPME!DqK3y zk;u2&b>Vcl(G9gu&&3Au7!{}P4v^8BG2FkWEiX>}9pD1)+(z^T%+U{1Aj^rzNKTEe zPN>?#)|O<|P?T?28*XK)>VES-rO##MvPlN2`^&ifO)5Fdv*3RNFI^ z!8B}8brLTHBi*+#Gt7MW1Wff~yU)r4gMk;OOIYD3LZ!y-zQL!vSt>yFgz7*(Tjb>^ z*_oi@uGnI;Y&?x(dOy zp^!xA)o4swSsBtpxh76U7ukpL zD7e`W&q-c`{ZN`q*J&6EdQT%9k&0`vQ&Rn6AX`0ivG!@xty zO_^q9wS$!a_(vA)7v7D0F4b8zS z)%E}%xGK(}@Dgk;aRtG}`)kO5Tf{Q=an_G$C{yZ0CEB`NvE{N^%foMI74cu2{x*<) zqH1^N`OyX2*0Bs4?Zb(gTS66NO3;#kzV<%6Cxe7QXC&9hhwO%>m@U8R5oObY+F9(m zN=nX(uTr%X`v7(aeKMS9U9vB~Q@x?ufw{ zi4Q)`-S}MnD>5~@leqVzV|V)k8>14|K4#{t@4KzD!K&dC|-Iacb_{NbFk^QM>K3R8lS2S)AN> zVEfw+FeCk^La5}j6j!J5v;NZ}=VJ-;?~^tbF|A;G+k|CGw{M)L59rvexjDem3gZwF0d2TF+M>YM$^;==&;#R#mcmEPT7P|n^; zo**r6dJe9cT6`x#^9HDM_Po8XsMg-bM2IqtX6RY?PnF)BpWfY=l7Y*D$WFzK^bf;+ zfXF2&u5_-cm7+-fU5GLRp+`zHIS>J?*mLS=aLi~-@6Y_NDi9->17sCBgm zk9#Tl=wGlsli<{HB$f6Gh^+#q>+6yj1$j`TzaZCS-I9kykEPu2Woz9!K0s|D>=FxO!|G?UYHQa} z!k79sCQ>`7wO|j8-ERm=;ZXg2ak`gfj9oC5AyJd zs_x6};}7SXgm0@;iFuoZ(@RT-iL()7Z}ROXLIuM#ghw24%kZnnyQbPjyNJ4CF&Qpf z?$*{UIf7hO0j1U6Dgke#eO`WO5KzeAZpu=0rBK3zm^)?)zICGC4Fbl}8?QD@f0j^@ zb25K-DtKHukAErIEIlz$Xkc>Qb)CD1Jnfo-Zys?JSQiO_doia&5okNTs~o8brk*9aoEAK5)IA?J-XFk2lM?Wc@u5gXt;i_MaGY0vhUaPG&)EPWL`oog+udaT zB6)Klpg5-`QRH2bEBJxSG2;8L!w1~EAd?@doK8=b@fu`2p~icXr)7#QQEl@ zGG=+o_W|V$q~(b6)o;lmae{Q|jgjJi)u)!P)QO`0mT~dU1I;qr8911$K@~?e!Nd|$BiRY+4A1%H%bRk!}QBOn^N0-s23Il4>-A!F@%~$XZEWn zO_^<#{O+;$);BHRg%5QSW;hb{7-fcZwHA)2vw+z?Xo}oMA~t3?ux_*x3@4*>Zqaf!z^Od7GSp$rMsaeFT ztAI0JQ3ht@RD|+>@qzliDtg|6k$YG5<(Yy2!bvT#?S?l5(?@m2okc;)Q5VA z%!;xRO6#E;l`jQ_nuQ5sL6%8bgHChioL>)nt@+mZ=A1d_J4RD53pU^R_Fj9f^S9rf#(zBAjl=Qo#ilkwCHfuwYkhF1B z3h-~Vk~IMO-zF5q_HknxWcl<)kzOlB7$zha_2ZH%XYN@ydw7RRa$6n+Sl^NN5uz>b0- zYNq2aU?Isd)+qiXFKFq`CgcS6Gu^5y9Q-YEdWSD*qc2}+*J@jaXa z@q7+^p1gl8O}w86pn!53@Nt_UT+t>nM=T=6ZIvu{03I`*DW>bwCLsL%E3!f`#`}!` zI0yl=fG=8r@MS&$oD=50n=_;qWZV^iUIq-BjQC~tdDk+I_nQHT6V3y^ZQ`Za_h6WN zLlgd*^$h`w$vpaaj^CdHt&ATJK%96n@IwQm+-u*Eg+vc#)>_o(%GDSjrWzheL*C6R#Tz)4f!--$hB zc{2|<&e(X)(};~9Agx>pINxMfNxRLaWwR3i5yUm-r3zzXxm8(=}Fu4y3%AX)+c7 zSLO7yY@M73%1T>nr|Tu{R<>tvmuelJvg4_Zr_R3#mvR_anK!a&A1I4`tOCiqRX8b^&-2c1!wN(gM}r z{hX4tQqpHRrH!gjyy&Vm7JGD``N|66@;<#xIq$P15U< z)>xl=+GY1kdQ;K{r8_CC-wnM&($<`eL~SN+nRMbhLF4_7Z;@Ard?=C5$b&iF2Lg}C zZbF>C2j-I9r!EHm$nmUq`-&v)=A5xk4rUm*l&mTV*k)s=$xp16x7-K+Y<^$@9rrzfW`@hoLq4^QY`NmK0i4AtkC+cPKF5M}sP zXs-{p0)S0=!3=OE9-n^aUy?Lc(j_*}6Lq`s_9)Anx0UW!yCofzv{%_3YLC)?1UrKb zQHJNn8<~ZzLua|g<$lunw^7>%dxZwB@F;0xWJErnz*2XM8RMbx53Qxbl>)&s&eX{zPIna?PNf>KOAoN*{bS4r|Q%> zP@_hT8a4hW7y+8@|JlH!z^A|;z%0<#Z(f-Odcb4zr1O4^XaTqYcn?TanX)C(@-uK$ zmVgGr&;nPQ86|FSIOPT@-vpkFlxz#C5KP6>r$LwmsI z1mZyd02~J#2223Q0?U9SfW^Qe+Ajh20e=E}f!)COz_-8;2$L}yTK@vDPRAcyiIz0( zR%4o34OJNNr44)qJP%w697$LOW9u3<)U#QY{Wo>aKIIJ$X~%y^dwwS>zO9=f@O^?m zc`N~5sc1x^GG>6230txcChS=Pt5u>ymk{DEck`}6wkL#Dd62YImT}xiB0c-yWAw`v z1jNhyuAA?0;BR0LaIw}Oq~8Zh$mnX>gZO7rv`dB3w<;K6D=-VJbV44F`rQ>7@kC^N z9x3a!e=hqTX5~U)dr2cq&{K|x70ABrk`9~qX`hzL`Y^```~Mjs5Da8LHUeNA_@uxH zTjt+@rEcuoks^tk<9cW4Nx&5ShnbB#Py3c)-@^tR4SY>DOTO-QH};~O{)yI21CsG0~)hxkCV zIy;=^*h$ED=JdcSg0ZxX5J|4kx=|$8-+|;h69eWM1I~B<=fwb1{u0t#W!5Rv z01ic7GC+*-rA0Up4R#x}H>a$?(jfg#RT-8QwvZPH%Qo%godV9x5SqoTf9T3%K7Lyo z)Lsf^^_^s=)ji->$|f&Gmus5@O{$}p*_S(!`)^kH0{8~F&M9YXZbW=%8h8~+pZ!DHgc8(79*Mo9+Z^{GL1G5E k_>7k^phk@vHEI;%U;0C8@{CnPx&QzG07*qoM6N<$g4?le1ONa4 literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-connecting.ico b/client/ui/assets/netbird-systemtray-connecting.ico deleted file mode 100644 index 4e4c3a9b114af61a28150b9fe2deaea75acdaa06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105091 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 OY{ZbUgZ~;x5BxvcE4?TH diff --git a/client/ui/assets/netbird-systemtray-disconnected-macos.png b/client/ui/assets/netbird-systemtray-disconnected-macos.png index 36b9a488f16673b1fcb1c87037d3d1606dfb1e5b..48cfa7c609bbf6e7cff2b3cec89a7eea90d31c47 100644 GIT binary patch literal 3474 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 F{|k6OYw7?1 literal 3491 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 VFbHp#EYMSdj14UGKVEZ=`9J&Jr*QxP diff --git a/client/ui/assets/netbird-systemtray-disconnected-mono-dark.png b/client/ui/assets/netbird-systemtray-disconnected-mono-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..035e71ba7f80e8a04caa63fc13b77bf2ed147381 GIT binary patch literal 1235 zcmV;^1T6cBP)yJ_qL}CyOhD9#t2|Ip2C{LXh-gq;1UGS!=pvv*R1ic^M8$<}ghV2U z?p*i+-S_~(MTigyVn8K9Fp94j-)5ZdzMqRzHMi!bANTgm*a%&4xOdLEr>g#{bL#x3 zCj<0aINc}FdSK_E#T`C#qxM;y(E*AOL|n&W1+v(!)`ZeOPY{$K+>TF z#mjgS`|`-O7)-m;2g=`u;{C0!zEt@WKPX;LX# zbCP~D1t%mOmvl_h*EZe_R7cX&=KP$bDb?S5x#@aB(huf$o}i_*^w1FN(Pa)e3LF4# z2iAo3J^?g=?e+~oyQcn*t^Ln|Dbt<#9*7(5Ih$}AXoc1etyOBBE(Q+=Ls9>^z)Jx{ zUJ-x;-fnuDQLjp!ZlG0BIp#=*lnH=66qV;{;L?(fhiiav=ilq*d<2l$U)hYV z`7!_pkpZiLgB3xzp*Mn#i-BKAORF8ed6LDMc)O&-#h2wNV1^>N0|6*}xEA=dhk>%8 z)4)cG>hMBoo}Zuq_XRsMz>lUQ76hMnKSed`ddl}Y`aNCif#ZEZc;7P=dua>ZD^wo( zXM?TXC0%{XW>o+Y#75w|pq%iY+t~ROnR~wi;G@C0Ch(c*ip%ZRk{`7J#MEsit;Yz$ z4ebu!Z|~#0i|?s>!@Ql==LD~3*{lm7g4hQ9MX?khmCYt8lGDH!6#Jr=c;-$3=TH=l zZ*6RzK4o(l01?DZr09C2V%dD46ybj28QK}IL8;`^F!urH_b!_x9ag%K zG&J~P;?gmSyhGBfDyVm+{+FXOoT)3$&6`TRd|%L0)p@CKayHCusD|^>@R#}+sUyP} zRsnXCJ##J2Y$X{MK&Es@m83nAUQxX2%T+Ifl7($%x0yLAJM~cSQlXk*yS@D<{#5+3?L85;=>GMybU%R|3EX!8zwVCn@+#Mmm&w(QyC! zmXh8T1>i<*>*AEZe*;_|=3EJUZ)0T!`|ff$UloA=t~m{SNq4sCjvWKmQ7nlqz^~z( z4g)UfD*?dykF(8QT;DhC5#WO0+%^*D@$r4j<$M(Yv1~fq?8o)p$&Y|l!O5Gg|4&lc ztjso-3cwwB(HyAqvtV53)1)&(z%GhQ`m#hH`m<4Iotq_nY^P*Zw%L-jO47@cc3GdF x(6!j+AX(0z*GD4b|B;Pa#sEewV*sO;e*xEJ!J}<>$UXo7002ovPDHLkV1lEZT7dun literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-disconnected-mono.png b/client/ui/assets/netbird-systemtray-disconnected-mono.png new file mode 100644 index 0000000000000000000000000000000000000000..d68c4dc5bbc9b83346dafb3f5c886e07dbb461a9 GIT binary patch literal 1088 zcmV-G1i$-^^SeR8zY_(t05mPIUcukB>xVsjm4*n$WEA`{3!rI%7hU-#@E0)Q zY%*-uM}ZZ<6Tl?!7%*;qOMwwH&;b4c{s1lmKLK;VFZMi-R1-J^?6LWyK7d5D%+>+m zcZ<8_2fS5qMSN=jmw=Cf7lBnSK&9EVjZxYZv9A>S-jUBu+4pV2ZSH;Z7hH?0LNmwV z1@c$flLK|ifnJy*`Ps1FubHhH@HFsvxQ0qI9P~Tx2UjsIfU!hvAf6DpV z9ob4Qo2ew$0#}{!xISM69wOVha|whmx!~%+cV^3{+m0xXQqb0IB;=zfAxFF2_~|ae ztLdHE>*mc^p9EgdvRQ`2X5c1SDyVEq(2W5*Wbw`+^&Bc-2^b8Y)c0s+Z|l;+FnNMv^)n6&FK z5o09W=hjhsV;o#Y6{Ug8<(q*_;n~yl#eTP;_WwQZnvA$u#iTX9YNoe(FBfEWt zoNOc$T`hvVm~vxQ5yV+1O|)ogvn+x0E;wnP>ty<+MmUr@$#DJN9@*_HM?d0H4r(_4Cr{3?c1w%I+fj zNFUhhL&(>8Le1~>4+yne1QI96{uD0^R4!DgP@zJFuJ|9-pl)IY;7^MH0000>!^2 literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-disconnected.ico b/client/ui/assets/netbird-systemtray-disconnected.ico deleted file mode 100644 index dcb9f4bf83dcfcb18d4858d5cf553895b092eca0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104575 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 Uw;PvXstfzveZt6u;jD=N1N46qGXMYp diff --git a/client/ui/assets/netbird-systemtray-error-dark.ico b/client/ui/assets/netbird-systemtray-error-dark.ico deleted file mode 100644 index 083816188d695c0a083ba7dcd78c1f31e9f06080..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105062 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 OdBMWYyyBea{eJ@L!Le-s literal 3837 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 VpKP%8!^Qvr002ovPDHLkV1iNR^nw5Y literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-error-mono.png b/client/ui/assets/netbird-systemtray-error-mono.png new file mode 100644 index 0000000000000000000000000000000000000000..164d65a4f20c892e9c852a5e2f978dfa1e7e7881 GIT binary patch literal 1449 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 OdBMWYyyBea{eJ@L!Le-s literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-needs-login-mono-dark.png b/client/ui/assets/netbird-systemtray-needs-login-mono-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6bcdacd447ee52d82d3a511abd5435b9a81297bc GIT binary patch literal 1571 zcmV+;2Hg3HP)lCl;3IO|$Nluj%-P?$v%h=y-s{>D?7%R;`}@tD^ZlMVbLPz4 z0-*nMDGoM(6D>mo2D=OqXt0#r^9)HVC4D35kfbh2ow2P}J7W7~uN2_Fs>Rq|3493r zL*u0kbOKkI+?L3rkW^oMoXz`&q;fx&FUCI0Bt5BY=6h=BO-r|=7D;bN+S&_f0QZ3w zWn{ci1OJMo``!0aDxgL&LmiVeP11oVr*{UID|B(+Hz76%-cbVSmh zlJ-g38|!Zh7D`J=!OBlX@Ewv`)%e;gM_Erx+8=R;S&zQ-SQGwewPXa@>W zZcFt2JAfZ!|Lz9m_e9_S2^bmWnfzV?1>p5K;5g7#XD#O_+y=CgEn2H*0Pi$G!usAG z<&?bKG(wnyt_Bp830CTezn731WjZi6(XpZd64vLdJn$)jA}|d2qzMu>&ASP1Q;4$m z0KWn^Mm?>uU0$b-?kM|;{s zewt4QzDor42mVth<4oXS1l~okB7|Te!t4mxR7HomZ_-fm}k(dJf5rvr*eKAF^I#5#y3px)} zIewc+X3>WdSvKw$;1rUuSuX+4`f}h%4hggVAz+LH^icxH_bmf+35&TD$Fve+y!Ekh zcT>an>mlGt%p&X$WoK1Ww8Ztlp$2#l%5QW67RK+E;BAS0YJ#tafLC&hvyi#N9zs+p zMrGR*0v`N4Di^R3DFCAhi{94BOzN3XwFU`@T? z8zJD8+jMV=zl*>q;CCv0-N4^V)FCHsG_U$KDm(^9d91ARY&>wEAE z67oKlr*VCxlb;7Tmqeok@)S=3{*IRSQRKb^yn&ku+?l*fffn4@#ZG4*63q~>X>W<` z$7CCofnS(_HrucPqvgl0-NlI1cYq}S7^bS3-t zB9+HYE9Of3f$JH788C%+RRN!p_#j#>RqDaN6#IJc-Bhj#fMaBluBkO({@VuZrSC#Z zi>28s;`rh8wS3-fMBd`H_9$>oH4^n&e`yrfN#?U@wVBxoOeTw3K6iO82fU42O}4Pp zec}jV{aG}ui-2{!koxcPcLC=ndtx7({awI$=?v5hs8naIcJwoa^PNjP>%jYQMq+|< zr=5f}rH_G5#tc-emd88-yy?K^kv0;OfiDO*c>aBF9mv0!2Bi%BPdwOVh`?Z%e*sEd VpKP%8!^Qvr002ovPDHLkV1iNR^nw5Y literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-needs-login-mono.png b/client/ui/assets/netbird-systemtray-needs-login-mono.png new file mode 100644 index 0000000000000000000000000000000000000000..164d65a4f20c892e9c852a5e2f978dfa1e7e7881 GIT binary patch literal 1449 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 Z(%>Cos#cWtK%X^)%YJvqTKk}k{{iS8*lPd) literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-update-connected-dark.ico b/client/ui/assets/netbird-systemtray-update-connected-dark.ico deleted file mode 100644 index b11bb54927fe232adf7924216fbbce9569d5156d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104704 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 diff --git a/client/ui/assets/netbird-systemtray-update-connected-macos.png b/client/ui/assets/netbird-systemtray-update-connected-macos.png index 8a6b2f2db85a6b6e312015ce634ec49096c45059..8b7b9f131f7581a4fc9d86c7d248735b2e6bce39 100644 GIT binary patch literal 3328 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 literal 3570 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 LNkvXXu0mjfn;@Q* literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-update-connected.ico b/client/ui/assets/netbird-systemtray-update-connected.ico deleted file mode 100644 index d3ce2f0f322f6df5c47adb9b46c883fe0eae86bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104698 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 MdJg!tzfZ*f0jyzlfdBvi diff --git a/client/ui/assets/netbird-systemtray-update-disconnected-dark.ico b/client/ui/assets/netbird-systemtray-update-disconnected-dark.ico deleted file mode 100644 index 123237f665722d0194d825033775bc90616a6a3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105086 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 WuRrb?z6NGGiIKzHh8`Q@9rAy92WzMR diff --git a/client/ui/assets/netbird-systemtray-update-disconnected-macos.png b/client/ui/assets/netbird-systemtray-update-disconnected-macos.png index 8b190034eac6588e58a36365880d83d9ea8d8db0..b6afa3937ae8e93102d69d1669c16f40844e8343 100644 GIT binary patch literal 3747 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 OSX)cSE7j({5B>{<{Ua~{ literal 3816 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 literal 0 HcmV?d00001 diff --git a/client/ui/assets/netbird-systemtray-update-disconnected.ico b/client/ui/assets/netbird-systemtray-update-disconnected.ico deleted file mode 100644 index 968dc4105c8fcc22249cc187ff1b2fc5786616af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105115 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 diff --git a/client/ui/assets/netbird.ico b/client/ui/assets/netbird.ico deleted file mode 100644 index 2bab8a503d92aad4dd9bb5eccc02f044762a7f6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 106176 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 + + + + + + + + + diff --git a/client/ui/assets/svg/netbird-menu.svg b/client/ui/assets/svg/netbird-menu.svg new file mode 100644 index 000000000..bd4e9d65d --- /dev/null +++ b/client/ui/assets/svg/netbird-menu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/ui/authsession/service.go b/client/ui/authsession/service.go new file mode 100644 index 000000000..28efe7cfd --- /dev/null +++ b/client/ui/authsession/service.go @@ -0,0 +1,116 @@ +//go:build !android && !ios && !freebsd && !js + +package authsession + +import ( + "context" + "time" + + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/proto" +) + +type ExtendStartParams struct { + // Hint is the OIDC login_hint, typically the user's email. + Hint string `json:"hint"` +} + +type ExtendStartResult struct { + VerificationURI string `json:"verificationUri"` + VerificationURIComplete string `json:"verificationUriComplete"` + UserCode string `json:"userCode"` + DeviceCode string `json:"deviceCode"` + ExpiresIn int64 `json:"expiresIn"` +} + +type ExtendWaitParams struct { + DeviceCode string `json:"deviceCode"` + UserCode string `json:"userCode"` +} + +// ExtendResult: ExpiresAt is nil when the peer is ineligible for extension. +// Preempted means a newer WaitExtend took over the IdP poll — a no-op, not a failure. +type ExtendResult struct { + ExpiresAt *time.Time `json:"sessionExpiresAt,omitempty"` + Preempted bool `json:"preempted,omitempty"` +} + +// DaemonConn duplicates services.DaemonConn to avoid an import cycle. +type DaemonConn interface { + Client() (proto.DaemonServiceClient, error) +} + +// Session bundles the session-auth daemon RPCs the UI drives. +type Session struct { + conn DaemonConn +} + +func NewSession(conn DaemonConn) *Session { + return &Session{conn: conn} +} + +// RequestExtend starts the SSO session-extension flow on the daemon. +func (s *Session) RequestExtend(ctx context.Context, p ExtendStartParams) (ExtendStartResult, error) { + cli, err := s.conn.Client() + if err != nil { + return ExtendStartResult{}, err + } + + req := &proto.RequestExtendAuthSessionRequest{} + if p.Hint != "" { + h := p.Hint + req.Hint = &h + } + + resp, err := cli.RequestExtendAuthSession(ctx, req) + if err != nil { + return ExtendStartResult{}, err + } + + return ExtendStartResult{ + VerificationURI: resp.GetVerificationURI(), + VerificationURIComplete: resp.GetVerificationURIComplete(), + UserCode: resp.GetUserCode(), + DeviceCode: resp.GetDeviceCode(), + ExpiresIn: resp.GetExpiresIn(), + }, nil +} + +// WaitExtend blocks until the user completes the SSO flow started by RequestExtend. +func (s *Session) WaitExtend(ctx context.Context, p ExtendWaitParams) (ExtendResult, error) { + cli, err := s.conn.Client() + if err != nil { + return ExtendResult{}, err + } + + resp, err := cli.WaitExtendAuthSession(ctx, &proto.WaitExtendAuthSessionRequest{ + DeviceCode: p.DeviceCode, + UserCode: p.UserCode, + }) + if err != nil { + if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Canceled { + return ExtendResult{Preempted: true}, nil + } + return ExtendResult{}, err + } + + out := ExtendResult{} + if ts := resp.GetSessionExpiresAt(); ts.IsValid() && !ts.AsTime().IsZero() { + t := ts.AsTime().UTC() + out.ExpiresAt = &t + } + return out, nil +} + +// DismissWarning suppresses the daemon's T-FinalWarningLead fallback dialog for +// the current deadline. Best-effort: a stale call is silently swallowed daemon-side. +func (s *Session) DismissWarning(ctx context.Context) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + _, err = cli.DismissSessionWarning(ctx, &proto.DismissSessionWarningRequest{}) + return err +} diff --git a/client/ui/authsession/warning.go b/client/ui/authsession/warning.go new file mode 100644 index 000000000..91ae7f101 --- /dev/null +++ b/client/ui/authsession/warning.go @@ -0,0 +1,64 @@ +//go:build !android && !ios && !freebsd && !js + +// Package authsession holds the UI-side domain logic for the SSO +// session-extend feature. The Wails facades in client/ui/services/session*.go +// are thin adapters over these types. +package authsession + +import ( + "time" + + "github.com/netbirdio/netbird/client/internal/auth/sessionwatch" +) + +// Re-exported from sessionwatch so UI-side consumers don't import the +// daemon-internal package directly. +const ( + 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 events. +type Warning struct { + // Absolute UTC deadline; best-effort, stays zero when metadata is + // missing or malformed (e.g. an older daemon) and the UI falls back + // to the Status snapshot. + ExpiresAt time.Time `json:"sessionExpiresAt"` + // Configured lead time, so the UI need not hardcode the constant. + LeadMinutes int `json:"leadMinutes"` + // True on the final-warning fallback event. + Final bool `json:"final"` +} + +// WarningFromMetadata parses SystemEvent metadata into a Warning, or returns +// (nil, false) when the event is not a session-warning. A field that fails to +// parse stays zero; the event is still surfaced. +func WarningFromMetadata(meta map[string]string) (*Warning, bool) { + if meta == nil || meta[MetaWarning] != "true" { + return nil, false + } + + out := &Warning{ + Final: meta[MetaFinal] == "true", + } + if raw := meta[MetaExpiresAt]; raw != "" { + if t, err := sessionwatch.ParseExpiresAt(raw); err == nil { + out.ExpiresAt = t + } + } + if raw := meta[MetaLeadMinutes]; raw != "" { + if n, err := sessionwatch.ParseLeadMinutes(raw); err == nil { + out.LeadMinutes = n + } + } + return out, true +} + +// ParseExpiresAt re-exports sessionwatch.ParseExpiresAt so UI-side call sites +// don't import the daemon-internal package. +func ParseExpiresAt(s string) (time.Time, error) { + return sessionwatch.ParseExpiresAt(s) +} diff --git a/client/ui/authsession/warning_test.go b/client/ui/authsession/warning_test.go new file mode 100644 index 000000000..297073ded --- /dev/null +++ b/client/ui/authsession/warning_test.go @@ -0,0 +1,82 @@ +//go:build !android && !ios && !freebsd && !js + +package authsession + +import ( + "testing" + "time" +) + +func TestWarningFromMetadata_NotASessionWarning(t *testing.T) { + cases := []struct { + name string + meta map[string]string + }{ + {"nil metadata", nil}, + {"empty map", map[string]string{}}, + {"unrelated event", map[string]string{"new_version_available": "0.65.0"}}, + {"flag not 'true'", map[string]string{"session_warning": "1"}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if w, ok := WarningFromMetadata(tc.meta); ok { + t.Fatalf("expected (nil, false), got (%+v, %v)", w, ok) + } + }) + } +} + +func TestWarningFromMetadata_FullPayload(t *testing.T) { + ts := "2026-05-18T13:30:00Z" + meta := map[string]string{ + "session_warning": "true", + "session_expires_at": ts, + "lead_minutes": "10", + } + + got, ok := WarningFromMetadata(meta) + if !ok { + t.Fatalf("expected the warning to be recognised, got ok=false") + } + want, _ := time.Parse(time.RFC3339, ts) + if !got.ExpiresAt.Equal(want.UTC()) { + t.Errorf("ExpiresAt = %v, want %v", got.ExpiresAt, want.UTC()) + } + if got.LeadMinutes != 10 { + t.Errorf("LeadMinutes = %d, want 10", got.LeadMinutes) + } +} + +func TestWarningFromMetadata_BadFieldsStillEmits(t *testing.T) { + // Older or buggy daemon: the flag is set but the timestamp/lead are + // missing or malformed. The UI should still get a warning so it can + // at least surface "session expires soon"; field zero-values are fine. + meta := map[string]string{ + "session_warning": "true", + "session_expires_at": "not-a-timestamp", + "lead_minutes": "abc", + } + + got, ok := WarningFromMetadata(meta) + if !ok { + t.Fatalf("warning should still be recognised even with malformed fields") + } + if !got.ExpiresAt.IsZero() { + t.Errorf("malformed timestamp should leave field zero, got %v", got.ExpiresAt) + } + if got.LeadMinutes != 0 { + t.Errorf("malformed lead_minutes should leave field 0, got %d", got.LeadMinutes) + } +} + +func TestWarningFromMetadata_MissingFieldsStillEmits(t *testing.T) { + // Only the flag is present (e.g. future-trimmed event). Still emit. + meta := map[string]string{"session_warning": "true"} + got, ok := WarningFromMetadata(meta) + if !ok { + t.Fatalf("warning should still be recognised when only flag is present") + } + if got.ExpiresAt.IsZero() != true || got.LeadMinutes != 0 { + t.Errorf("missing fields should be zero-valued, got %+v", got) + } +} diff --git a/client/ui/build/Taskfile.yml b/client/ui/build/Taskfile.yml new file mode 100644 index 000000000..590d4791b --- /dev/null +++ b/client/ui/build/Taskfile.yml @@ -0,0 +1,295 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - pnpm-lock.yaml + generates: + - node_modules + preconditions: + - sh: pnpm --version + msg: "Looks like pnpm isn't installed. Install with: corepack enable && corepack prepare pnpm@latest --activate" + cmds: + - pnpm install + + build:frontend: + label: build:frontend (DEV={{.DEV}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + - exclude: node_modules/**/* + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - pnpm run {{.BUILD_COMMAND}} + env: + PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}' + vars: + BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}' + + + frontend:vendor:puppertino: + summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling + sources: + - frontend/public/puppertino/puppertino.css + generates: + - frontend/public/puppertino/puppertino.css + cmds: + - | + set -euo pipefail + mkdir -p frontend/public/puppertino + # If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error. + if [ ! -f frontend/public/puppertino/puppertino.css ]; then + echo "No bundled Puppertino found. Attempting to fetch from GitHub..." + if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then + curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true + echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css" + else + echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it." + fi + else + echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css" + fi + # Ensure index.html includes Puppertino CSS and button classes + INDEX_HTML=frontend/index.html + if [ -f "$INDEX_HTML" ]; then + if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then + # Insert Puppertino link tag after style.css link + awk ' + /href="\/style.css"\/?/ && !x { print; print " "; x=1; next }1 + ' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML" + fi + # Replace default .btn with Puppertino primary button classes if present + sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true + fi + + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms). + dir: build + sources: + - "appicon.png" + - "appicon.icon" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin + + generate:tray:icons: + summary: Rebuild Windows multi-res .ico files from the per-state PNGs. + desc: | + The colored tray PNGs (assets/netbird-systemtray-.png) and the + macOS template variants are committed to the repo as the canonical + source. This task only regenerates the Windows multi-resolution .ico + files from those PNGs by downscaling each to 16/24/32/48 px and + packing them with icotool, so Shell_NotifyIcon picks the frame + matching the user's DPI instead of downscaling a single large PNG. + + Run after replacing any of the colored PNGs (e.g. when copying a new + version of the icons from client/ui/assets). The SVG sources in + assets/svg/ are kept for reference but are not built by default. + dir: assets + sources: + - "netbird-systemtray-connected.png" + - "netbird-systemtray-disconnected.png" + - "netbird-systemtray-connecting.png" + - "netbird-systemtray-error.png" + - "netbird-systemtray-update-connected.png" + - "netbird-systemtray-update-disconnected.png" + generates: + - "netbird-systemtray-*.ico" + preconditions: + - sh: command -v magick >/dev/null 2>&1 || command -v convert >/dev/null 2>&1 + msg: "ImageMagick is required to downscale PNGs (apt install imagemagick)" + - sh: command -v icotool >/dev/null 2>&1 + msg: "icotool is required to pack tray .ico files (apt install icoutils)" + cmds: + - | + set -euo pipefail + tmp=$(mktemp -d) + trap 'rm -rf "$tmp"' EXIT + resize=$(command -v magick || echo convert) + for state in connected disconnected connecting error update-connected update-disconnected; do + for sz in 16 24 32 48; do + "$resize" "netbird-systemtray-$state.png" -resize ${sz}x${sz} "$tmp/$state-$sz.png" + done + icotool -c -o "netbird-systemtray-$state.ico" \ + "$tmp/$state-16.png" "$tmp/$state-24.png" "$tmp/$state-32.png" "$tmp/$state-48.png" + done + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - pnpm exec vite --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + desc: | + Builds the application with the server build tag enabled. + Server mode runs as a pure HTTP server without native GUI dependencies. + Usage: task build:server + deps: + - task: build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - go build -tags server {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}} + vars: + BUILD_FLAGS: "{{.BUILD_FLAGS}}" + + run:server: + summary: Builds and runs the application in server mode + deps: + - task: build:server + cmds: + - ./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}} + + build:docker: + summary: Builds a Docker image for server mode deployment + desc: | + Creates a minimal Docker image containing the server mode binary. + The image is based on distroless for security and small size. + Usage: task build:docker [TAG=myapp:latest] + cmds: + - docker build -t {{.TAG | default (printf "%s:latest" .APP_NAME)}} -f build/docker/Dockerfile.server . + vars: + TAG: "{{.TAG}}" + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + - sh: test -f build/docker/Dockerfile.server + msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it." + + run:docker: + summary: Builds and runs the Docker image + desc: | + Builds the Docker image and runs it, exposing port 8080. + Usage: task run:docker [TAG=myapp:latest] [PORT=8080] + Note: The internal container port is always 8080. The PORT variable + only changes the host port mapping. Ensure your app uses port 8080 + or modify the Dockerfile to match your ServerOptions.Port setting. + deps: + - task: build:docker + vars: + TAG: + ref: .TAG + cmds: + - docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}} + vars: + TAG: "{{.TAG}}" + PORT: "{{.PORT}}" + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + desc: | + Builds the Docker image needed for cross-compiling to any platform. + Run this once to enable cross-platform builds from any OS. + cmds: + - docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/ + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + + ios:device:list: + summary: Lists connected iOS devices (UDIDs) + cmds: + - xcrun xcdevice list + + ios:run:device: + summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl) + vars: + PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/.xcodeproj + SCHEME: '{{.SCHEME}}' # e.g., ios.dev + CONFIG: '{{.CONFIG | default "Debug"}}' + DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}' + UDID: '{{.UDID}}' # from `task ios:device:list` + BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev + TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing + preconditions: + - sh: xcrun -f xcodebuild + msg: "xcodebuild not found. Please install Xcode." + - sh: xcrun -f devicectl + msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)." + - sh: test -n '{{.PROJECT}}' + msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)." + - sh: test -n '{{.SCHEME}}' + msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)." + - sh: test -n '{{.UDID}}' + msg: "Set UDID to your device UDID (see: task ios:device:list)." + - sh: test -n '{{.BUNDLE_ID}}' + msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)." + cmds: + - | + set -euo pipefail + echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}" + XCB_ARGS=( + -project "{{.PROJECT}}" + -scheme "{{.SCHEME}}" + -configuration "{{.CONFIG}}" + -destination "id={{.UDID}}" + -derivedDataPath "{{.DERIVED}}" + -allowProvisioningUpdates + -allowProvisioningDeviceRegistration + ) + # Optionally inject signing identifiers if provided + if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi + if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi + xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true + # If xcpretty isn't installed, run without it + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + xcodebuild "${XCB_ARGS[@]}" build + fi + # Find built .app + APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1) + if [ -z "$APP_PATH" ]; then + echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2 + exit 1 + fi + echo "Installing: $APP_PATH" + xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH" + echo "Launching: {{.BUNDLE_ID}}" + xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}" diff --git a/client/ui/build/appicon.icon/Assets/wails_icon_vector.svg b/client/ui/build/appicon.icon/Assets/wails_icon_vector.svg new file mode 100644 index 000000000..83c4c22a9 --- /dev/null +++ b/client/ui/build/appicon.icon/Assets/wails_icon_vector.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/client/ui/build/appicon.icon/icon.json b/client/ui/build/appicon.icon/icon.json new file mode 100644 index 000000000..4a0371af3 --- /dev/null +++ b/client/ui/build/appicon.icon/icon.json @@ -0,0 +1,26 @@ +{ + "fill" : { + "solid" : "srgb:1.00000,1.00000,1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "image-name" : "wails_icon_vector.svg", + "name" : "wails_icon_vector" + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "specular" : true + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} diff --git a/client/ui/build/appicon.png b/client/ui/build/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..977d2400a04587b0ced0c7ee5218b4ee3940e021 GIT binary patch literal 90556 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 W$z{*+P_040KUF2I+xZISFa95i8Dww( literal 0 HcmV?d00001 diff --git a/client/ui/build/build-ui-linux.sh b/client/ui/build/build-ui-linux.sh deleted file mode 100644 index eab08214d..000000000 --- a/client/ui/build/build-ui-linux.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -sudo apt update -sudo apt remove gir1.2-appindicator3-0.1 -sudo apt install -y libayatana-appindicator3-dev -go build \ No newline at end of file diff --git a/client/ui/build/config.yml b/client/ui/build/config.yml new file mode 100644 index 000000000..08b95b6bd --- /dev/null +++ b/client/ui/build/config.yml @@ -0,0 +1,78 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "NetBird GmbH" # The name of the company + productName: "NetBird" # The name of the application + productIdentifier: "io.netbird.client" # The unique product identifier + description: "NetBird desktop client" # The application description + copyright: "NetBird GmbH" # Copyright text + comments: "Some Product Comments" # Comments + version: "0.0.1" # The application version + # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) + # # Should match the name of your .icon file without the extension + # # If not set and Assets.car exists, defaults to "appicon" + +# iOS build configuration (uncomment to customise iOS project generation) +# Note: Keys under `ios` OVERRIDE values under `info` when set. +# ios: +# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier) +# bundleID: "com.mycompany.myproduct" +# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName) +# displayName: "My Product" +# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion) +# version: "0.0.1" +# # The company/organisation name for templates and project settings +# company: "My Company" +# # Additional comments to embed in Info.plist metadata +# comments: "Some Product Comments" + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + watched_extension: + - "*.go" + - "*.js" # Watch for changes to JS/TS files included using the //wails:include directive. + - "*.ts" # The frontend directory will be excluded entirely by the setting above. + git_ignore: true + executes: + - cmd: wails3 build DEV=true + type: blocking + - cmd: wails3 task common:dev:frontend + type: background + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/client/ui/build/darwin/Info.dev.plist b/client/ui/build/darwin/Info.dev.plist new file mode 100644 index 000000000..78f5a7b1c --- /dev/null +++ b/client/ui/build/darwin/Info.dev.plist @@ -0,0 +1,38 @@ + + + + CFBundlePackageType + APPL + CFBundleName + NetBird + CFBundleDisplayName + NetBird + CFBundleExecutable + netbird-ui + CFBundleIdentifier + io.netbird.client + CFBundleVersion + 0.0.1 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.0.1 + CFBundleIconFile + icons + CFBundleIconName + appicon + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + LSUIElement + 1 + NSHumanReadableCopyright + NetBird GmbH + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/client/ui/build/darwin/Info.plist b/client/ui/build/darwin/Info.plist new file mode 100644 index 000000000..1e12b049b --- /dev/null +++ b/client/ui/build/darwin/Info.plist @@ -0,0 +1,36 @@ + + + + CFBundlePackageType + APPL + CFBundleName + NetBird + CFBundleDisplayName + NetBird + CFBundleExecutable + netbird-ui + CFBundleIdentifier + io.netbird.client + CFBundleVersion + 0.0.1 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.0.1 + CFBundleIconFile + icons + CFBundleIconName + appicon + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + + LSUIElement + 1 + NSHumanReadableCopyright + NetBird GmbH + + \ No newline at end of file diff --git a/client/ui/build/darwin/Taskfile.yml b/client/ui/build/darwin/Taskfile.yml new file mode 100644 index 000000000..8a5c27bdc --- /dev/null +++ b/client/ui/build/darwin/Taskfile.yml @@ -0,0 +1,210 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)" + # KEYCHAIN_PROFILE: "my-notarize-profile" + # ENTITLEMENTS: "build/darwin/entitlements.plist" + + # Docker image for cross-compilation (used when building on non-macOS) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application + cmds: + - task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + OUTPUT: '{{.OUTPUT}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' + vars: + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + + build:native: + summary: Builds the application natively on macOS + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + + build:docker: + summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + # Handles both relative (=> ../) and absolute (=> /) paths + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}' + + build:universal:lipo:native: + summary: Creates universal binary using native lipo (macOS) + internal: true + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + build:universal:lipo:go: + summary: Creates universal binary using wails3 tool lipo (Linux/Windows) + internal: true + cmds: + - wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages the application into a `.app` bundle + deps: + - task: build + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + fi + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents" + - task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}' + + codesign:adhoc: + summary: Ad-hoc signs the app bundle (macOS only) + internal: true + cmds: + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" + + codesign:skip: + summary: Skips codesigning when cross-compiling + internal: true + cmds: + - 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."' + + run: + deps: + - task: common:generate:icons + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + fi + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist" + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' + + sign: + summary: Signs the application bundle with Developer ID + desc: | + Signs the .app bundle for distribution. + Configure SIGN_IDENTITY in the vars section at the top of this file. + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_IDENTITY}}" ]' + msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" + + sign:notarize: + summary: Signs and notarizes the application bundle + desc: | + Signs the .app bundle and submits it for notarization. + Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file. + + Setup (one-time): + wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile" + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}} + preconditions: + - sh: '[ -n "{{.SIGN_IDENTITY}}" ]' + msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" + - sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]' + msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" diff --git a/client/ui/build/darwin/icons.icns b/client/ui/build/darwin/icons.icns new file mode 100644 index 0000000000000000000000000000000000000000..fb78a18a98a10de8864b2652083800a679808444 GIT binary patch literal 141191 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

vT@rDm(OaHzFd(h}w@UJg|bH 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 literal 0 HcmV?d00001 diff --git a/client/ui/build/docker/Dockerfile.cross b/client/ui/build/docker/Dockerfile.cross new file mode 100644 index 000000000..a487b8db0 --- /dev/null +++ b/client/ui/build/docker/Dockerfile.cross @@ -0,0 +1,203 @@ +# Cross-compile Wails v3 apps to any platform +# +# Darwin: Zig + macOS SDK +# Linux: Native GCC when host matches target, Zig for cross-arch +# Windows: Zig + bundled mingw +# +# Usage: +# docker build -t wails-cross -f Dockerfile.cross . +# docker run --rm -v $(pwd):/app wails-cross darwin arm64 +# docker run --rm -v $(pwd):/app wails-cross darwin amd64 +# docker run --rm -v $(pwd):/app wails-cross linux amd64 +# docker run --rm -v $(pwd):/app wails-cross linux arm64 +# 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 + +ARG TARGETARCH + +# Install base tools, GCC, and GTK/WebKit dev packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl xz-utils nodejs npm pkg-config gcc libc6-dev \ + libgtk-3-dev libwebkit2gtk-4.1-dev \ + libgtk-4-dev libwebkitgtk-6.0-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Zig - automatically selects correct binary for host architecture +ARG ZIG_VERSION=0.14.0 +RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ + curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" \ + | tar -xJ -C /opt \ + && ln -s /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig + +# Download macOS SDK (required for darwin targets) +ARG MACOS_SDK_VERSION=14.5 +RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \ + | tar -xJ -C /opt \ + && mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk + +ENV MACOS_SDK_PATH=/opt/macos-sdk + +# Create Zig CC wrappers for cross-compilation targets +# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch) + +# Darwin arm64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -mmacosx-version-min=*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-darwin-arm64 + +# Darwin amd64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -mmacosx-version-min=*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-darwin-amd64 + +# Windows amd64 - uses Zig's bundled mingw +COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -Wl,*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target x86_64-windows-gnu $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-windows-amd64 + +# Windows arm64 - uses Zig's bundled mingw +COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -Wl,*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target aarch64-windows-gnu $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-windows-arm64 + +# Build script +COPY <<'SCRIPT' /usr/local/bin/build.sh +#!/bin/sh +set -e + +OS=${1:-darwin} +ARCH=${2:-arm64} + +case "${OS}-${ARCH}" in + darwin-arm64|darwin-aarch64) + export CC=zcc-darwin-arm64 + export GOARCH=arm64 + export GOOS=darwin + ;; + darwin-amd64|darwin-x86_64) + export CC=zcc-darwin-amd64 + export GOARCH=amd64 + export GOOS=darwin + ;; + linux-arm64|linux-aarch64) + export CC=gcc + export GOARCH=arm64 + export GOOS=linux + ;; + linux-amd64|linux-x86_64) + export CC=gcc + export GOARCH=amd64 + export GOOS=linux + ;; + windows-arm64|windows-aarch64) + export CC=zcc-windows-arm64 + export GOARCH=arm64 + export GOOS=windows + ;; + windows-amd64|windows-x86_64) + export CC=zcc-windows-amd64 + export GOARCH=amd64 + export GOOS=windows + ;; + *) + echo "Usage: " + echo " os: darwin, linux, windows" + echo " arch: amd64, arm64" + exit 1 + ;; +esac + +export CGO_ENABLED=1 +export CGO_CFLAGS="-w" + +# Build frontend if exists and not already built (host may have built it) +if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then + (cd frontend && npm install --silent && npm run build --silent) +fi + +# Build +APP=${APP_NAME:-$(basename $(pwd))} +mkdir -p bin + +EXT="" +LDFLAGS="-s -w" +if [ "$GOOS" = "windows" ]; then + EXT=".exe" + LDFLAGS="-s -w -H windowsgui" +fi + +TAGS="production" +if [ -n "$EXTRA_TAGS" ]; then + TAGS="${TAGS},${EXTRA_TAGS}" +fi + +go build -tags "$TAGS" -trimpath -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} . +echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}" +SCRIPT +RUN chmod +x /usr/local/bin/build.sh + +WORKDIR /app +ENTRYPOINT ["/usr/local/bin/build.sh"] +CMD ["darwin", "arm64"] diff --git a/client/ui/build/docker/Dockerfile.server b/client/ui/build/docker/Dockerfile.server new file mode 100644 index 000000000..58fb64f76 --- /dev/null +++ b/client/ui/build/docker/Dockerfile.server @@ -0,0 +1,41 @@ +# Wails Server Mode Dockerfile +# Multi-stage build for minimal image size + +# Build stage +FROM golang:alpine AS builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache git + +# Copy source code +COPY . . + +# Remove local replace directive if present (for production builds) +RUN sed -i '/^replace/d' go.mod || true + +# Download dependencies +RUN go mod tidy + +# Build the server binary +RUN go build -tags server -ldflags="-s -w" -o server . + +# Runtime stage - minimal image +FROM gcr.io/distroless/static-debian12 + +# Copy the binary +COPY --from=builder /app/server /server + +# Copy frontend assets +COPY --from=builder /app/frontend/dist /frontend/dist + +# Expose the default port +EXPOSE 8080 + +# Bind to all interfaces (required for Docker) +# Can be overridden at runtime with -e WAILS_SERVER_HOST=... +ENV WAILS_SERVER_HOST=0.0.0.0 + +# Run the server +ENTRYPOINT ["/server"] diff --git a/client/ui/build/linux/Taskfile.yml b/client/ui/build/linux/Taskfile.yml new file mode 100644 index 000000000..94d041375 --- /dev/null +++ b/client/ui/build/linux/Taskfile.yml @@ -0,0 +1,235 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # PGP_KEY: "path/to/signing-key.asc" + # SIGN_ROLE: "builder" # Options: origin, maint, archive, builder + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation (used when building on non-Linux or no CC available) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Linux + cmds: + # Linux requires CGO - use Docker when: + # 1. Cross-compiling from non-Linux, OR + # 2. No C compiler is available, OR + # 3. Target architecture differs from host architecture (cross-arch compilation) + - task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + OUTPUT: '{{.OUTPUT}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' + vars: + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Determine target architecture (defaults to host ARCH if not specified) + TARGET_ARCH: '{{.ARCH | default ARCH}}' + # Check if a C compiler is available (gcc or clang) + HAS_CC: + sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"' + + build:native: + summary: Builds the application natively on Linux + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + - task: generate:dotdesktop + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + + build:docker: + summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + - task: generate:dotdesktop + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation to Linux. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + package: + summary: Packages the application for Linux + deps: + - task: build + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + - task: generate:dotdesktop + cmds: + - cp "{{.APP_BINARY}}" "{{.APP_NAME}}" + - cp ../../appicon.png "{{.APP_NAME}}.png" + - wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: '{{.APP_NAME}}' + APP_BINARY: '../../../bin/{{.APP_NAME}}' + ICON: '{{.APP_NAME}}.png' + DESKTOP_FILE: '../{{.APP_NAME}}.desktop' + OUTPUT_DIR: '../../../bin' + + create:deb: + summary: Creates a deb package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}" + # Wrap Exec= with `env WEBKIT_DISABLE_DMABUF_RENDERER=1 ...` so launches + # from any desktop environment use the working renderer. See build/linux/Taskfile.yml :run for the matching dev-mode env block. + - sed -i -E 's|^Exec=([^ ]+)(.*)$|Exec=env WEBKIT_DISABLE_DMABUF_RENDERER=1 \1\2|' {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop + vars: + APP_NAME: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: '{{.APP_NAME}}' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' + env: + # WebKitGTK 2.50's default DMA-BUF renderer fails on RDP, VirtualBox/QEMU, + # and some bare WMs (Fluxbox, dwm) where DRM dumb-buffer access is + # restricted. Disabling it falls back to the GLES2/cairo path which works + # everywhere. Production launchers must set this too. + WEBKIT_DISABLE_DMABUF_RENDERER: "1" + + sign:deb: + summary: Signs the DEB package + desc: | + Signs the .deb package with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:deb + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}} + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" + + sign:rpm: + summary: Signs the RPM package + desc: | + Signs the .rpm package with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:rpm + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}} + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" + + sign:packages: + summary: Signs all Linux packages (DEB and RPM) + desc: | + Signs both .deb and .rpm packages with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + cmds: + - task: sign:deb + - task: sign:rpm + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" diff --git a/client/ui/build/linux/appimage/build.sh b/client/ui/build/linux/appimage/build.sh new file mode 100644 index 000000000..85901c34e --- /dev/null +++ b/client/ui/build/linux/appimage/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" + diff --git a/client/ui/build/linux/desktop b/client/ui/build/linux/desktop new file mode 100644 index 000000000..deadfe9f4 --- /dev/null +++ b/client/ui/build/linux/desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name=NetBird +Comment=NetBird desktop client +# The Exec line includes %u to pass the URL to the application +Exec=/usr/local/bin/netbird-ui %u +Terminal=false +Type=Application +Icon=netbird-ui +Categories=Utility; +StartupWMClass=netbird-ui + + diff --git a/client/ui/build/linux/netbird-ui.desktop b/client/ui/build/linux/netbird-ui.desktop new file mode 100755 index 000000000..6b6ed42a5 --- /dev/null +++ b/client/ui/build/linux/netbird-ui.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=netbird-ui +Exec=env WEBKIT_DISABLE_DMABUF_RENDERER=1 netbird-ui +Icon=netbird-ui +Categories=Development; +Terminal=false +Keywords=wails +Version=1.0 +StartupNotify=false diff --git a/client/ui/build/netbird.desktop b/client/ui/build/linux/netbird.desktop similarity index 54% rename from client/ui/build/netbird.desktop rename to client/ui/build/linux/netbird.desktop index b3a1b92dc..a81f3698a 100644 --- a/client/ui/build/netbird.desktop +++ b/client/ui/build/linux/netbird.desktop @@ -1,8 +1,9 @@ [Desktop Entry] Name=Netbird -Exec=/usr/bin/netbird-ui +Exec=env WEBKIT_DISABLE_DMABUF_RENDERER=1 /usr/bin/netbird-ui Icon=netbird Type=Application Terminal=false Categories=Utility; Keywords=netbird; +StartupWMClass=org.wails.netbird \ No newline at end of file diff --git a/client/ui/build/linux/nfpm/nfpm.yaml b/client/ui/build/linux/nfpm/nfpm.yaml new file mode 100644 index 000000000..a05daef62 --- /dev/null +++ b/client/ui/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,70 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "netbird-ui" +arch: ${GOARCH} +platform: "linux" +version: "0.0.1" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "NetBird desktop client" +vendor: "NetBird" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/netbird-ui" + dst: "/usr/local/bin/netbird-ui" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/netbird-ui.png" + - src: "./build/linux/netbird-ui.desktop" + dst: "/usr/share/applications/netbird-ui.desktop" + +# Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+) +depends: + - libgtk-4-1 + - libwebkitgtk-6.0-4 + - xdg-utils + +# Distribution-specific overrides for different package formats +overrides: + # RPM packages for Fedora / RHEL / AlmaLinux / Rocky Linux + rpm: + depends: + - gtk4 + - webkitgtk6.0 + - xdg-utils + + # Arch Linux packages + archlinux: + depends: + - gtk4 + - webkitgtk-6.0 + - xdg-utils + +# scripts section to ensure desktop database is updated after install +scripts: + postinstall: "./build/linux/nfpm/scripts/postinstall.sh" + # You can also add preremove, postremove if needed + # preremove: "./build/linux/nfpm/scripts/preremove.sh" + # postremove: "./build/linux/nfpm/scripts/postremove.sh" + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" diff --git a/client/ui/build/linux/nfpm/scripts/postinstall.sh b/client/ui/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 000000000..4bbb815a3 --- /dev/null +++ b/client/ui/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# Update desktop database for .desktop file changes +# This makes the application appear in application menus and registers its capabilities. +if command -v update-desktop-database >/dev/null 2>&1; then + echo "Updating desktop database..." + update-desktop-database -q /usr/share/applications +else + echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2 +fi + +# Update MIME database for custom URL schemes (x-scheme-handler) +# This ensures the system knows how to handle your custom protocols. +if command -v update-mime-database >/dev/null 2>&1; then + echo "Updating MIME database..." + update-mime-database -n /usr/share/mime +else + echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2 +fi + +exit 0 diff --git a/client/ui/build/linux/nfpm/scripts/postremove.sh b/client/ui/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/client/ui/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/client/ui/build/linux/nfpm/scripts/preinstall.sh b/client/ui/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/client/ui/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/client/ui/build/linux/nfpm/scripts/preremove.sh b/client/ui/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/client/ui/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/client/ui/build/windows/Taskfile.yml b/client/ui/build/windows/Taskfile.yml new file mode 100644 index 000000000..f51f7fbee --- /dev/null +++ b/client/ui/build/windows/Taskfile.yml @@ -0,0 +1,243 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_CERTIFICATE: "path/to/certificate.pfx" + # SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE + # TIMESTAMP_SERVER: "http://timestamp.digicert.com" + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Windows + cmds: + # CGO Windows builds from Linux use mingw-w64 (lighter than docker). + # Docker is only needed if mingw-w64 is unavailable. + - task: build:native + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' + vars: + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + + build:console: + summary: Builds a console-attached Windows binary so logs go to the terminal. + desc: | + Same as `windows:build` but links against the console PE subsystem + instead of windowsgui, so stdout/stderr (logrus, panics) print to the + terminal that launched the .exe. Useful for chasing tray, event-stream, + or daemon-RPC bugs that have no other feedback channel on Windows. + + Output is bin/netbird-ui-console.exe — kept distinct so the production + binary built by `windows:build` isn't shadowed. + + Cross-compile from Linux works the same way: + CGO_ENABLED=1 task windows:build:console + + Pass DEV=true to drop the `production` build tag so the WebKit/WebView2 + DevTools inspector (right-click → Inspect, or F12) stays enabled and the + frontend JS console is reachable — same DEV handling as windows:build: + CGO_ENABLED=1 task windows:build:console DEV=true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + preconditions: + - sh: '[ "{{OS}}" = "windows" ] || [ "{{.CGO_ENABLED}}" != "1" ] || command -v {{.CC}}' + msg: "{{.CC}} not found. Install with: sudo apt-get install gcc-mingw-w64-x86-64 (Debian/Ubuntu) / sudo dnf install mingw64-gcc (Fedora)" + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}-console.exe" + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + # Identical to build:native's flags (including DEV handling) except no + # -H windowsgui, so the binary attaches to the launching console. With + # DEV=true the `production` tag is dropped, keeping the WebKit/WebView2 + # DevTools inspector enabled so the frontend JS console is reachable. + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -ldflags="-w -s"{{end}}' + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + CC: '{{.CC | default "x86_64-w64-mingw32-gcc"}}' + env: + GOOS: windows + CGO_ENABLED: '{{.CGO_ENABLED}}' + GOARCH: '{{.ARCH | default ARCH}}' + CC: '{{.CC}}' + + build:native: + summary: Builds for Windows natively, or cross-compiles from Linux/macOS via mingw-w64. + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + preconditions: + # When cross-compiling with CGO from a non-Windows host, the mingw-w64 + # cross-gcc must be present. Native Windows builds skip this check. + - sh: '[ "{{OS}}" = "windows" ] || [ "{{.CGO_ENABLED}}" != "1" ] || command -v {{.CC}}' + msg: "{{.CC}} not found. Install with: sudo apt-get install gcc-mingw-w64-x86-64 (Debian/Ubuntu) / sudo dnf install mingw64-gcc (Fedora)" + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe" + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -ldflags="-w -s -H windowsgui"{{end}}' + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + CC: '{{.CC | default "x86_64-w64-mingw32-gcc"}}' + env: + GOOS: windows + CGO_ENABLED: '{{.CGO_ENABLED}}' + GOARCH: '{{.ARCH | default ARCH}}' + CC: '{{.CC}}' + + build:docker: + summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for CGO cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - task: generate:syso + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - rm -f *.syso + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + package: + summary: Packages the application + cmds: + - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}' + vars: + FORMAT: '{{.FORMAT | default "nsis"}}' + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - | + {{if eq OS "windows"}} + makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi + {{else}} + makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + {{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + create:msix:package: + summary: Creates an MSIX package + deps: + - task: build + cmds: + - |- + wails3 tool msix \ + --config "{{.ROOT_DIR}}/wails.json" \ + --name "{{.APP_NAME}}" \ + --executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \ + --arch "{{.ARCH}}" \ + --out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \ + {{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \ + {{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \ + {{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + CERT_PATH: '{{.CERT_PATH | default ""}}' + PUBLISHER: '{{.PUBLISHER | default ""}}' + USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}' + + install:msix:tools: + summary: Installs tools required for MSIX packaging + cmds: + - wails3 tool msix-install-tools + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' + + sign: + summary: Signs the Windows executable + desc: | + Signs the .exe with an Authenticode certificate. + Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: build + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]' + msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml" + + sign:installer: + summary: Signs the NSIS installer + desc: | + Creates and signs the NSIS installer. + Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:nsis:installer + cmds: + - wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]' + msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml" diff --git a/client/ui/build/windows/icon.ico b/client/ui/build/windows/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7abbfa5a3276e654a567589fbafc3c2907a5076e GIT binary patch literal 18126 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 literal 0 HcmV?d00001 diff --git a/client/ui/build/windows/info.json b/client/ui/build/windows/info.json new file mode 100644 index 000000000..a67c8fd81 --- /dev/null +++ b/client/ui/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.0.1" + }, + "info": { + "0000": { + "ProductVersion": "0.0.1", + "CompanyName": "NetBird", + "FileDescription": "NetBird desktop client", + "LegalCopyright": "NetBird GmbH", + "ProductName": "NetBird", + "Comments": "This is a comment" + } + } +} \ No newline at end of file diff --git a/client/ui/build/windows/msix/app_manifest.xml b/client/ui/build/windows/msix/app_manifest.xml new file mode 100644 index 000000000..0ae55ce77 --- /dev/null +++ b/client/ui/build/windows/msix/app_manifest.xml @@ -0,0 +1,55 @@ + + + + + + + NetBird + NetBird + NetBird desktop client + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/ui/build/windows/msix/template.xml b/client/ui/build/windows/msix/template.xml new file mode 100644 index 000000000..437a68097 --- /dev/null +++ b/client/ui/build/windows/msix/template.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + false + NetBird + NetBird + NetBird desktop client + Assets\AppIcon.png + + + + + + + diff --git a/client/ui/build/windows/nsis/project.nsi b/client/ui/build/windows/nsis/project.nsi new file mode 100644 index 000000000..8d2530972 --- /dev/null +++ b/client/ui/build/windows/nsis/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "netbird-ui" +## !define INFO_COMPANYNAME "My Company" # Default "NetBird" +## !define INFO_PRODUCTNAME "My Product Name" # Default "NetBird" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.0.1" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/client/ui/build/windows/nsis/wails_tools.nsh b/client/ui/build/windows/nsis/wails_tools.nsh new file mode 100644 index 000000000..b63101b32 --- /dev/null +++ b/client/ui/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,236 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "netbird-ui" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "NetBird" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "NetBird" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.0.1" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "NetBird GmbH" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + +!macroend \ No newline at end of file diff --git a/client/ui/build/windows/wails.exe.manifest b/client/ui/build/windows/wails.exe.manifest new file mode 100644 index 000000000..f8b7b8e14 --- /dev/null +++ b/client/ui/build/windows/wails.exe.manifest @@ -0,0 +1,22 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + + + + + + + diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go deleted file mode 100644 index d2f38cfd7..000000000 --- a/client/ui/client_ui.go +++ /dev/null @@ -1,1986 +0,0 @@ -//go:build !(linux && 386) - -package main - -import ( - "context" - _ "embed" - "errors" - "flag" - "fmt" - "net/url" - "os" - "os/exec" - "os/user" - "path" - "runtime" - "strconv" - "strings" - "sync" - "time" - "unicode" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" - "fyne.io/systray" - "github.com/cenkalti/backoff/v4" - log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - - "github.com/netbirdio/netbird/client/iface" - "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/internal/profilemanager" - "github.com/netbirdio/netbird/client/mdm" - "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/client/ui/desktop" - "github.com/netbirdio/netbird/client/ui/event" - "github.com/netbirdio/netbird/client/ui/notifier" - "github.com/netbirdio/netbird/client/ui/process" - "github.com/netbirdio/netbird/util" - - "github.com/netbirdio/netbird/version" -) - -const ( - defaultFailTimeout = 3 * time.Second - failFastTimeout = time.Second -) - -const ( - censoredPreSharedKey = "**********" - maxSSHJWTCacheTTL = 86_400 // 24 hours in seconds - // mdmFieldSuffix is appended to plain-text Entry widgets in the - // advanced Settings window when the underlying field is enforced - // by MDM, so the user sees the lock indicator inline next to the - // value. Stripped before any read site that feeds the value back - // into a SetConfig request (saveSettings / parseNumericSettings). - mdmFieldSuffix = " (MDM)" -) - -// main is the entry point for the UI tray/client binary. Parses CLI -// flags, initialises logging, builds the Fyne application and tray -// icons, and constructs the service client (which may open a -// requested UI window). When a window-mode flag is set the Fyne event -// loop runs and main returns; otherwise main enforces single-instance -// behaviour (signalling an existing instance to show its window when -// present), sets up signal handling + default fonts, and runs the -// system tray loop. -func main() { - flags := parseFlags() - - // Initialize file logging if needed. - var logFile string - if flags.saveLogsInFile { - file, err := initLogFile() - if err != nil { - log.Errorf("error while initializing log: %v", err) - return - } - logFile = file - } else { - _ = util.InitLog("trace", util.LogConsole) - } - - // Create the Fyne application. - a := app.NewWithID("NetBird") - a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnected)) - - // Show error message window if needed. - if flags.errorMsg != "" { - showErrorMessage(flags.errorMsg) - return - } - - // Create the service client (this also builds the settings or networks UI if requested). - client := newServiceClient(&newServiceClientArgs{ - addr: flags.daemonAddr, - logFile: logFile, - app: a, - showSettings: flags.showSettings, - showNetworks: flags.showNetworks, - showLoginURL: flags.showLoginURL, - showDebug: flags.showDebug, - showProfiles: flags.showProfiles, - showQuickActions: flags.showQuickActions, - showUpdate: flags.showUpdate, - showUpdateVersion: flags.showUpdateVersion, - }) - - // Watch for theme/settings changes to update the icon. - go watchSettingsChanges(a, client) - - // Run in window mode if any UI flag was set. - if flags.showSettings || flags.showNetworks || flags.showDebug || flags.showLoginURL || flags.showProfiles || flags.showQuickActions || flags.showUpdate { - a.Run() - return - } - - // Check for another running process. - pid, running, err := process.IsAnotherProcessRunning() - if err != nil { - log.Errorf("error while checking process: %v", err) - return - } - if running { - log.Infof("another process is running with pid %d, sending signal to show window", pid) - if err := sendShowWindowSignal(pid); err != nil { - log.Errorf("send signal to running instance: %v", err) - } - return - } - - client.setupSignalHandler(client.ctx) - - client.setDefaultFonts() - systray.Run(client.onTrayReady, client.onTrayExit) -} - -type cliFlags struct { - daemonAddr string - showSettings bool - showNetworks bool - showProfiles bool - showDebug bool - showLoginURL bool - showQuickActions bool - errorMsg string - saveLogsInFile bool - showUpdate bool - showUpdateVersion string -} - -// parseFlags reads and returns all needed command-line flags. -func parseFlags() *cliFlags { - var flags cliFlags - - defaultDaemonAddr := "unix:///var/run/netbird.sock" - if runtime.GOOS == "windows" { - defaultDaemonAddr = "tcp://127.0.0.1:41731" - } - flag.StringVar(&flags.daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") - flag.BoolVar(&flags.showSettings, "settings", false, "run settings window") - flag.BoolVar(&flags.showNetworks, "networks", false, "run networks window") - flag.BoolVar(&flags.showProfiles, "profiles", false, "run profiles window") - flag.BoolVar(&flags.showDebug, "debug", false, "run debug window") - flag.BoolVar(&flags.showQuickActions, "quick-actions", false, "run quick actions window") - flag.StringVar(&flags.errorMsg, "error-msg", "", "displays an error message window") - flag.BoolVar(&flags.saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir())) - flag.BoolVar(&flags.showLoginURL, "login-url", false, "show login URL in a popup window") - flag.BoolVar(&flags.showUpdate, "update", false, "show update progress window") - flag.StringVar(&flags.showUpdateVersion, "update-version", "", "version to update to") - flag.Parse() - return &flags -} - -// initLogFile initializes logging into a file. -func initLogFile() (string, error) { - logFile := path.Join(os.TempDir(), fmt.Sprintf("netbird-ui-%d.log", os.Getpid())) - return logFile, util.InitLog("trace", logFile) -} - -// watchSettingsChanges listens for Fyne theme/settings changes and updates the client icon. -func watchSettingsChanges(a fyne.App, client *serviceClient) { - a.Settings().AddListener(func(settings fyne.Settings) { - client.updateIcon() - }) -} - -// showErrorMessage displays an error message in a simple window. -func showErrorMessage(msg string) { - a := app.New() - w := a.NewWindow("NetBird Error") - label := widget.NewLabel(msg) - label.Wrapping = fyne.TextWrapWord - w.SetContent(label) - w.Resize(fyne.NewSize(400, 100)) - w.Show() - a.Run() -} - -//go:embed assets/netbird-systemtray-connected-macos.png -var iconConnectedMacOS []byte - -//go:embed assets/netbird-systemtray-disconnected-macos.png -var iconDisconnectedMacOS []byte - -//go:embed assets/netbird-systemtray-update-disconnected-macos.png -var iconUpdateDisconnectedMacOS []byte - -//go:embed assets/netbird-systemtray-update-connected-macos.png -var iconUpdateConnectedMacOS []byte - -//go:embed assets/netbird-systemtray-connecting-macos.png -var iconConnectingMacOS []byte - -//go:embed assets/netbird-systemtray-error-macos.png -var iconErrorMacOS []byte - -//go:embed assets/connected.png -var iconConnectedDot []byte - -//go:embed assets/disconnected.png -var iconDisconnectedDot []byte - -type serviceClient struct { - ctx context.Context - cancel context.CancelFunc - addr string - conn proto.DaemonServiceClient - connLock sync.Mutex - - eventHandler *eventHandler - - profileManager *profilemanager.ProfileManager - - icAbout []byte - icConnected []byte - icConnectedDot []byte - icDisconnected []byte - icDisconnectedDot []byte - icUpdateConnected []byte - icUpdateDisconnected []byte - icConnecting []byte - icError []byte - - // systray menu items - mStatus *systray.MenuItem - mUp *systray.MenuItem - mDown *systray.MenuItem - mSettings *systray.MenuItem - mProfile *profileMenu - mAbout *systray.MenuItem - mGitHub *systray.MenuItem - mVersionUI *systray.MenuItem - mVersionDaemon *systray.MenuItem - mUpdate *systray.MenuItem - mQuit *systray.MenuItem - mNetworks *systray.MenuItem - mAllowSSH *systray.MenuItem - mAutoConnect *systray.MenuItem - mEnableRosenpass *systray.MenuItem - mLazyConnEnabled *systray.MenuItem - mBlockInbound *systray.MenuItem - mNotifications *systray.MenuItem - mAdvancedSettings *systray.MenuItem - mCreateDebugBundle *systray.MenuItem - mExitNode *systray.MenuItem - - // application with main windows. - app fyne.App - notifier notifier.Notifier - wSettings fyne.Window - showAdvancedSettings bool - sendNotification bool - - // input elements for settings form - iMngURL *widget.Entry - iLogFile *widget.Entry - iPreSharedKey *widget.Entry - iInterfaceName *widget.Entry - iInterfacePort *widget.Entry - iMTU *widget.Entry - - // switch elements for settings form - sRosenpassPermissive *widget.Check - sNetworkMonitor *widget.Check - sDisableDNS *widget.Check - sDisableClientRoutes *widget.Check - sDisableServerRoutes *widget.Check - sDisableIPv6 *widget.Check - sBlockLANAccess *widget.Check - sEnableSSHRoot *widget.Check - sEnableSSHSFTP *widget.Check - sEnableSSHLocalPortForward *widget.Check - sEnableSSHRemotePortForward *widget.Check - sDisableSSHAuth *widget.Check - iSSHJWTCacheTTL *widget.Entry - - // observable settings over corresponding iMngURL and iPreSharedKey values. - managementURL string - preSharedKey string - - RosenpassPermissive bool - interfaceName string - interfacePort int - mtu uint16 - networkMonitor bool - disableDNS bool - disableClientRoutes bool - disableServerRoutes bool - disableIPv6 bool - blockLANAccess bool - enableSSHRoot bool - enableSSHSFTP bool - enableSSHLocalPortForward bool - enableSSHRemotePortForward bool - disableSSHAuth bool - sshJWTCacheTTL int - - connected bool - daemonVersion string - updateIndicationLock sync.Mutex - isUpdateIconActive bool - isEnforcedUpdate bool - lastNotifiedVersion string - profilesEnabled bool - networksEnabled bool - // networksMenuEnabled caches the last applied enabled-state of the - // mNetworks + mExitNode submenu items. Combines features.DisableNetworks - // AND s.connected — both must be true for the menus to be active. - // Zero value (false) matches the Disable() call at AddMenuItem time. - networksMenuEnabled bool - showNetworks bool - wNetworks fyne.Window - wProfiles fyne.Window - wQuickActions fyne.Window - - eventManager *event.Manager - - exitNodeMu sync.Mutex - mExitNodeItems []menuHandler - exitNodeRetryCancel context.CancelFunc - mExitNodeSeparator *systray.MenuItem - mExitNodeDeselectAll *systray.MenuItem - logFile string - wLoginURL fyne.Window - wUpdateProgress fyne.Window - updateContextCancel context.CancelFunc - - connectCancel context.CancelFunc - - // mdmManagedFields caches the names of MDM-enforced policy keys - // surfaced by the daemon in GetConfigResponse. Each refresh of - // daemon config (loadSettings, getSrvConfig, config_changed event) - // updates this set and re-applies the lock/badge to the affected - // menu items and settings-form widgets. - mdmManagedFields map[string]bool -} - -type menuHandler struct { - *systray.MenuItem - cancel context.CancelFunc -} - -type newServiceClientArgs struct { - addr string - logFile string - app fyne.App - showSettings bool - showNetworks bool - showDebug bool - showLoginURL bool - showProfiles bool - showQuickActions bool - showUpdate bool - showUpdateVersion string -} - -// newServiceClient instance constructor -// -// This constructor also builds the UI elements for the settings window. -func newServiceClient(args *newServiceClientArgs) *serviceClient { - ctx, cancel := context.WithCancel(context.Background()) - s := &serviceClient{ - ctx: ctx, - cancel: cancel, - addr: args.addr, - app: args.app, - notifier: notifier.New(args.app), - logFile: args.logFile, - sendNotification: false, - - showAdvancedSettings: args.showSettings, - showNetworks: args.showNetworks, - networksEnabled: true, - } - - s.eventHandler = newEventHandler(s) - s.profileManager = profilemanager.NewProfileManager() - s.setNewIcons() - - switch { - case args.showSettings: - s.showSettingsUI() - case args.showNetworks: - s.showNetworksUI() - case args.showLoginURL: - s.showLoginURL() - case args.showDebug: - s.showDebugUI() - case args.showProfiles: - s.showProfilesUI() - case args.showQuickActions: - s.showQuickActionsUI() - case args.showUpdate: - s.showUpdateProgress(ctx, args.showUpdateVersion) - } - - return s -} - -func (s *serviceClient) setNewIcons() { - s.icAbout = iconAbout - s.icConnectedDot = iconConnectedDot - s.icDisconnectedDot = iconDisconnectedDot - if s.app.Settings().ThemeVariant() == theme.VariantDark { - s.icConnected = iconConnectedDark - s.icDisconnected = iconDisconnected - s.icUpdateConnected = iconUpdateConnectedDark - s.icUpdateDisconnected = iconUpdateDisconnectedDark - s.icConnecting = iconConnectingDark - s.icError = iconErrorDark - } else { - s.icConnected = iconConnected - s.icDisconnected = iconDisconnected - s.icUpdateConnected = iconUpdateConnected - s.icUpdateDisconnected = iconUpdateDisconnected - s.icConnecting = iconConnecting - s.icError = iconError - } -} - -func (s *serviceClient) updateIcon() { - s.setNewIcons() - s.updateIndicationLock.Lock() - if s.connected { - if s.isUpdateIconActive { - systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) - } else { - systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) - } - } else { - if s.isUpdateIconActive { - systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) - } else { - systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) - } - } - s.updateIndicationLock.Unlock() -} - -func (s *serviceClient) showSettingsUI() { - // DisableUpdateSettings no longer gates the window from opening: - // the daemon blocks every actual mutation at SetConfig / Login, - // so the window is safe to show as a read-only view. The previous - // early-return also blocked Advanced Settings whenever update - // editing was off, which conflated two distinct kill switches - // (see comment in checkAndUpdateFeatures). - - // add settings window UI elements. - s.wSettings = s.app.NewWindow("NetBird Settings") - s.wSettings.SetOnClosed(s.cancel) - - s.iMngURL = widget.NewEntry() - - s.iLogFile = widget.NewEntry() - s.iLogFile.Disable() - s.iPreSharedKey = widget.NewPasswordEntry() - s.iInterfaceName = widget.NewEntry() - s.iInterfacePort = widget.NewEntry() - s.iMTU = widget.NewEntry() - - s.sRosenpassPermissive = widget.NewCheck("Enable Rosenpass permissive mode", nil) - - s.sNetworkMonitor = widget.NewCheck("Restarts NetBird when the network changes", nil) - s.sDisableDNS = widget.NewCheck("Keeps system DNS settings unchanged", nil) - s.sDisableClientRoutes = widget.NewCheck("This peer won't route traffic to other peers", nil) - s.sDisableServerRoutes = widget.NewCheck("This peer won't act as router for others", nil) - s.sDisableIPv6 = widget.NewCheck("Disable IPv6 overlay addressing", nil) - s.sBlockLANAccess = widget.NewCheck("Blocks local network access when used as exit node", nil) - s.sEnableSSHRoot = widget.NewCheck("Enable SSH Root Login", nil) - s.sEnableSSHSFTP = widget.NewCheck("Enable SSH SFTP", nil) - s.sEnableSSHLocalPortForward = widget.NewCheck("Enable SSH Local Port Forwarding", nil) - s.sEnableSSHRemotePortForward = widget.NewCheck("Enable SSH Remote Port Forwarding", nil) - s.sDisableSSHAuth = widget.NewCheck("Disable SSH Authentication", nil) - s.iSSHJWTCacheTTL = widget.NewEntry() - - s.wSettings.SetContent(s.getSettingsForm()) - s.wSettings.Resize(fyne.NewSize(600, 400)) - s.wSettings.SetFixedSize(true) - - s.getSrvConfig() - s.wSettings.Show() -} - -func (s *serviceClient) getConnectionForm() *widget.Form { - var activeProfName string - activeProf, err := s.profileManager.GetActiveProfile() - if err != nil { - log.Errorf("get active profile: %v", err) - } else { - activeProfName = activeProf.Name - } - return &widget.Form{ - Items: []*widget.FormItem{ - {Text: "Profile", Widget: widget.NewLabel(activeProfName)}, - {Text: "Management URL", Widget: s.iMngURL}, - {Text: "Pre-shared Key", Widget: s.iPreSharedKey}, - {Text: "Quantum-Resistance", Widget: s.sRosenpassPermissive}, - {Text: "Interface Name", Widget: s.iInterfaceName}, - {Text: "Interface Port", Widget: s.iInterfacePort, HintText: "If set to 0, a random free port will be used"}, - {Text: "MTU", Widget: s.iMTU}, - {Text: "Log File", Widget: s.iLogFile}, - }, - } -} - -func (s *serviceClient) saveSettings() { - // Check if update settings are disabled by daemon - features, err := s.getFeatures() - if err != nil { - log.Errorf("failed to get features from daemon: %v", err) - // Continue with default behavior if features can't be retrieved - } else if features != nil && features.DisableUpdateSettings { - log.Warn("Configuration updates are disabled by daemon") - dialog.ShowError(fmt.Errorf("configuration updates are disabled by daemon"), s.wSettings) - return - } - - if err := s.validateSettings(); err != nil { - dialog.ShowError(err, s.wSettings) - return - } - - port, mtu, err := s.parseNumericSettings() - if err != nil { - dialog.ShowError(err, s.wSettings) - return - } - - iMngURL := strings.TrimSpace(strings.TrimSuffix(s.iMngURL.Text, mdmFieldSuffix)) - - if s.hasSettingsChanged(iMngURL, port, mtu) { - if err := s.applySettingsChanges(iMngURL, port, mtu); err != nil { - dialog.ShowError(err, s.wSettings) - return - } - } - - s.wSettings.Close() -} - -func (s *serviceClient) validateSettings() error { - if s.iPreSharedKey.Text != "" && s.iPreSharedKey.Text != censoredPreSharedKey { - if _, err := wgtypes.ParseKey(s.iPreSharedKey.Text); err != nil { - return fmt.Errorf("invalid pre-shared key value") - } - } - return nil -} - -func (s *serviceClient) parseNumericSettings() (int64, int64, error) { - port, err := strconv.ParseInt(strings.TrimSpace(strings.TrimSuffix(s.iInterfacePort.Text, mdmFieldSuffix)), 10, 64) - if err != nil { - return 0, 0, errors.New("invalid interface port") - } - if port < 0 || port > 65535 { - return 0, 0, errors.New("invalid interface port: out of range 0-65535") - } - - var mtu int64 - mtuText := strings.TrimSpace(s.iMTU.Text) - if mtuText != "" { - mtu, err = strconv.ParseInt(mtuText, 10, 64) - if err != nil { - return 0, 0, errors.New("invalid MTU value") - } - if mtu < iface.MinMTU || mtu > iface.MaxMTU { - return 0, 0, fmt.Errorf("MTU must be between %d and %d bytes", iface.MinMTU, iface.MaxMTU) - } - } - - return port, mtu, nil -} - -func (s *serviceClient) hasSettingsChanged(iMngURL string, port, mtu int64) bool { - return s.managementURL != iMngURL || - s.preSharedKey != s.iPreSharedKey.Text || - s.RosenpassPermissive != s.sRosenpassPermissive.Checked || - s.interfaceName != s.iInterfaceName.Text || - s.interfacePort != int(port) || - s.mtu != uint16(mtu) || - s.networkMonitor != s.sNetworkMonitor.Checked || - s.disableDNS != s.sDisableDNS.Checked || - s.disableClientRoutes != s.sDisableClientRoutes.Checked || - s.disableServerRoutes != s.sDisableServerRoutes.Checked || - s.disableIPv6 != s.sDisableIPv6.Checked || - s.blockLANAccess != s.sBlockLANAccess.Checked || - s.hasSSHChanges() -} - -func (s *serviceClient) applySettingsChanges(iMngURL string, port, mtu int64) error { - s.managementURL = iMngURL - s.preSharedKey = s.iPreSharedKey.Text - s.mtu = uint16(mtu) - - req, err := s.buildSetConfigRequest(iMngURL, port, mtu) - if err != nil { - return fmt.Errorf("build config request: %w", err) - } - - if err := s.sendConfigUpdate(req); err != nil { - return fmt.Errorf("set configuration: %w", err) - } - - return nil -} - -func (s *serviceClient) buildSetConfigRequest(iMngURL string, port, mtu int64) (*proto.SetConfigRequest, error) { - currUser, err := user.Current() - if err != nil { - return nil, fmt.Errorf("get current user: %w", err) - } - - activeProf, err := s.profileManager.GetActiveProfile() - if err != nil { - return nil, fmt.Errorf("get active profile: %w", err) - } - - req := &proto.SetConfigRequest{ - ProfileName: activeProf.ID.String(), - Username: currUser.Username, - } - - if iMngURL != "" { - req.ManagementUrl = iMngURL - } - - req.RosenpassPermissive = &s.sRosenpassPermissive.Checked - req.InterfaceName = &s.iInterfaceName.Text - req.WireguardPort = &port - if mtu > 0 { - req.Mtu = &mtu - } - - req.NetworkMonitor = &s.sNetworkMonitor.Checked - req.DisableDns = &s.sDisableDNS.Checked - req.DisableClientRoutes = &s.sDisableClientRoutes.Checked - req.DisableServerRoutes = &s.sDisableServerRoutes.Checked - req.DisableIpv6 = &s.sDisableIPv6.Checked - req.BlockLanAccess = &s.sBlockLANAccess.Checked - - req.EnableSSHRoot = &s.sEnableSSHRoot.Checked - req.EnableSSHSFTP = &s.sEnableSSHSFTP.Checked - req.EnableSSHLocalPortForwarding = &s.sEnableSSHLocalPortForward.Checked - req.EnableSSHRemotePortForwarding = &s.sEnableSSHRemotePortForward.Checked - req.DisableSSHAuth = &s.sDisableSSHAuth.Checked - - sshJWTCacheTTLText := strings.TrimSpace(s.iSSHJWTCacheTTL.Text) - if sshJWTCacheTTLText != "" { - sshJWTCacheTTL, err := strconv.ParseInt(sshJWTCacheTTLText, 10, 32) - if err != nil { - return nil, errors.New("invalid SSH JWT Cache TTL value") - } - if sshJWTCacheTTL < 0 || sshJWTCacheTTL > maxSSHJWTCacheTTL { - return nil, fmt.Errorf("SSH JWT Cache TTL must be between 0 and %d seconds", maxSSHJWTCacheTTL) - } - sshJWTCacheTTL32 := int32(sshJWTCacheTTL) - req.SshJWTCacheTTL = &sshJWTCacheTTL32 - } - - // Only attach the PSK when the user actually typed something: - // - "" means the field was left untouched (we deliberately render - // an empty Text + placeholder hint to avoid leaking the daemon's - // "**********" redaction through the password reveal toggle); - // sending an empty pointer would tell the daemon to clear / overwrite - // the on-disk or MDM-enforced PSK, which then trips the MDM - // conflict gate when PSK is policy-managed. - // - "**********" is the redacted echo (legacy non-MDM path); also a no-op. - if s.iPreSharedKey.Text != "" && s.iPreSharedKey.Text != censoredPreSharedKey { - req.OptionalPreSharedKey = &s.iPreSharedKey.Text - } - - return req, nil -} - -func (s *serviceClient) sendConfigUpdate(req *proto.SetConfigRequest) error { - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - return fmt.Errorf("get client: %w", err) - } - - _, err = conn.SetConfig(s.ctx, req) - if err != nil { - return fmt.Errorf("set config: %w", err) - } - - // Reconnect if connected to apply the new settings. - // Use a background context so the reconnect outlives the settings window. - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - status, err := conn.Status(ctx, &proto.StatusRequest{}) - if err != nil { - log.Errorf("failed to get service status: %v", err) - return - } - if status.Status == string(internal.StatusConnected) { - if _, err = conn.Down(ctx, &proto.DownRequest{}); err != nil { - log.Errorf("failed to stop service: %v", err) - } - // TODO: wait for the service to be idle before calling Up, or use a fresh connection - if _, err = conn.Up(ctx, &proto.UpRequest{}); err != nil { - log.Errorf("failed to start service: %v", err) - } - } - }() - - return nil -} - -func (s *serviceClient) getSettingsForm() fyne.CanvasObject { - connectionForm := s.getConnectionForm() - networkForm := s.getNetworkForm() - sshForm := s.getSSHForm() - tabs := container.NewAppTabs( - container.NewTabItem("Connection", connectionForm), - container.NewTabItem("Network", networkForm), - container.NewTabItem("SSH", sshForm), - ) - saveButton := widget.NewButtonWithIcon("Save", theme.ConfirmIcon(), s.saveSettings) - saveButton.Importance = widget.HighImportance - cancelButton := widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func() { - s.wSettings.Close() - }) - buttonContainer := container.NewHBox( - layout.NewSpacer(), - cancelButton, - saveButton, - ) - return container.NewBorder(nil, buttonContainer, nil, nil, tabs) -} - -func (s *serviceClient) getNetworkForm() *widget.Form { - return &widget.Form{ - Items: []*widget.FormItem{ - {Text: "Network Monitor", Widget: s.sNetworkMonitor}, - {Text: "Disable DNS", Widget: s.sDisableDNS}, - {Text: "Disable Client Routes", Widget: s.sDisableClientRoutes}, - {Text: "Disable Server Routes", Widget: s.sDisableServerRoutes}, - {Text: "Disable IPv6", Widget: s.sDisableIPv6}, - {Text: "Disable LAN Access", Widget: s.sBlockLANAccess}, - }, - } -} - -func (s *serviceClient) getSSHForm() *widget.Form { - return &widget.Form{ - Items: []*widget.FormItem{ - {Text: "Enable SSH Root Login", Widget: s.sEnableSSHRoot}, - {Text: "Enable SSH SFTP", Widget: s.sEnableSSHSFTP}, - {Text: "Enable SSH Local Port Forwarding", Widget: s.sEnableSSHLocalPortForward}, - {Text: "Enable SSH Remote Port Forwarding", Widget: s.sEnableSSHRemotePortForward}, - {Text: "Disable SSH Authentication", Widget: s.sDisableSSHAuth}, - {Text: "JWT Cache TTL (seconds, 0=disabled)", Widget: s.iSSHJWTCacheTTL}, - }, - } -} - -func (s *serviceClient) hasSSHChanges() bool { - currentSSHJWTCacheTTL := s.sshJWTCacheTTL - if text := strings.TrimSpace(s.iSSHJWTCacheTTL.Text); text != "" { - val, err := strconv.Atoi(text) - if err != nil { - return true - } - currentSSHJWTCacheTTL = val - } - - return s.enableSSHRoot != s.sEnableSSHRoot.Checked || - s.enableSSHSFTP != s.sEnableSSHSFTP.Checked || - s.enableSSHLocalPortForward != s.sEnableSSHLocalPortForward.Checked || - s.enableSSHRemotePortForward != s.sEnableSSHRemotePortForward.Checked || - s.disableSSHAuth != s.sDisableSSHAuth.Checked || - s.sshJWTCacheTTL != currentSSHJWTCacheTTL -} - -func (s *serviceClient) login(ctx context.Context, openURL bool) (*proto.LoginResponse, error) { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return nil, fmt.Errorf("get daemon client: %w", err) - } - - activeProf, err := s.profileManager.GetActiveProfile() - if err != nil { - return nil, fmt.Errorf("get active profile: %w", err) - } - - currUser, err := user.Current() - if err != nil { - return nil, fmt.Errorf("get current user: %w", err) - } - - handle := activeProf.ID.String() - - loginReq := &proto.LoginRequest{ - IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", - ProfileName: &handle, - Username: &currUser.Username, - } - - profileState, err := s.profileManager.GetProfileState(activeProf.ID) - if err != nil { - log.Debugf("failed to get profile state for login hint: %v", err) - } else if profileState.Email != "" { - loginReq.Hint = &profileState.Email - } - - loginResp, err := conn.Login(ctx, loginReq) - if err != nil { - return nil, fmt.Errorf("login to management: %w", err) - } - - if loginResp.NeedsSSOLogin && openURL { - if err = s.handleSSOLogin(ctx, loginResp, conn); err != nil { - return nil, fmt.Errorf("SSO login: %w", err) - } - } - - return loginResp, nil -} - -func (s *serviceClient) handleSSOLogin(ctx context.Context, loginResp *proto.LoginResponse, conn proto.DaemonServiceClient) error { - if err := openURL(loginResp.VerificationURIComplete); err != nil { - return fmt.Errorf("open browser: %w", err) - } - - resp, err := conn.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode}) - if err != nil { - return fmt.Errorf("wait for SSO login: %w", err) - } - - if resp.Email != "" { - if err := s.profileManager.SetActiveProfileState(&profilemanager.ProfileState{ - Email: resp.Email, - }); err != nil { - log.Debugf("failed to set profile state: %v", err) - } else { - s.mProfile.refresh() - } - } - - return nil -} - -func (s *serviceClient) menuUpClick(ctx context.Context) error { - systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - systray.SetTemplateIcon(iconErrorMacOS, s.icError) - return fmt.Errorf("get daemon client: %w", err) - } - - _, err = s.login(ctx, true) - if err != nil { - return fmt.Errorf("login: %w", err) - } - - status, err := conn.Status(ctx, &proto.StatusRequest{}) - if err != nil { - return fmt.Errorf("get status: %w", err) - } - - if status.Status == string(internal.StatusConnected) { - return nil - } - - if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil { - return fmt.Errorf("start connection: %w", err) - } - - return nil -} - -func (s *serviceClient) menuDownClick() error { - systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return fmt.Errorf("get daemon client: %w", err) - } - - status, err := conn.Status(s.ctx, &proto.StatusRequest{}) - if err != nil { - return fmt.Errorf("get status: %w", err) - } - - if status.Status != string(internal.StatusConnected) && status.Status != string(internal.StatusConnecting) { - return nil - } - - if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil { - return fmt.Errorf("stop connection: %w", err) - } - - return nil -} - -func (s *serviceClient) updateStatus() error { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return err - } - err = backoff.Retry(func() error { - status, err := conn.Status(s.ctx, &proto.StatusRequest{}) - if err != nil { - log.Errorf("get service status: %v", err) - if s.connected { - s.notifier.Send("Error", "Connection to service lost") - } - s.setDisconnectedStatus() - return err - } - - s.updateIndicationLock.Lock() - defer s.updateIndicationLock.Unlock() - - // notify the user when the session has expired - if status.Status == string(internal.StatusSessionExpired) { - s.onSessionExpire() - } - - var systrayIconState bool - - switch { - case status.Status == string(internal.StatusConnected) && !s.connected: - s.connected = true - s.sendNotification = true - if s.isUpdateIconActive { - systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) - } else { - systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) - } - systray.SetTooltip("NetBird (Connected)") - s.mStatus.SetTitle("Connected") - s.mStatus.SetIcon(s.icConnectedDot) - s.mUp.Disable() - s.mDown.Enable() - if s.networksEnabled { - s.mNetworks.Enable() - s.mExitNode.Enable() - } - s.startExitNodeRefresh() - systrayIconState = true - case status.Status == string(internal.StatusConnecting): - s.setConnectingStatus() - case status.Status != string(internal.StatusConnected) && s.mUp.Disabled(): - s.setDisconnectedStatus() - systrayIconState = false - } - - // if the daemon version changed (e.g. after a successful update), reset the update indication - if s.daemonVersion != status.DaemonVersion { - if s.daemonVersion != "" { - s.mUpdate.Hide() - s.isUpdateIconActive = false - } - s.daemonVersion = status.DaemonVersion - if !s.isUpdateIconActive { - if systrayIconState { - systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) - } else { - systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) - } - } - - daemonVersionTitle := normalizedVersion(s.daemonVersion) - s.mVersionDaemon.SetTitle(fmt.Sprintf("Daemon: %s", daemonVersionTitle)) - s.mVersionDaemon.SetTooltip(fmt.Sprintf("Daemon version: %s", daemonVersionTitle)) - s.mVersionDaemon.Show() - } - - return nil - }, &backoff.ExponentialBackOff{ - InitialInterval: time.Second, - RandomizationFactor: backoff.DefaultRandomizationFactor, - Multiplier: backoff.DefaultMultiplier, - MaxInterval: 300 * time.Millisecond, - MaxElapsedTime: 2 * time.Second, - Stop: backoff.Stop, - Clock: backoff.SystemClock, - }) - if err != nil { - return err - } - - return nil -} - -func (s *serviceClient) setDisconnectedStatus() { - s.connected = false - if s.isUpdateIconActive { - systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) - } else { - systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) - } - systray.SetTooltip("NetBird (Disconnected)") - s.mStatus.SetTitle("Disconnected") - s.mStatus.SetIcon(s.icDisconnectedDot) - s.mDown.Disable() - s.mUp.Enable() - s.mNetworks.Disable() - s.mExitNode.Disable() - s.cancelExitNodeRetry() - go s.updateExitNodes() -} - -func (s *serviceClient) setConnectingStatus() { - s.connected = false - systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) - systray.SetTooltip("NetBird (Connecting)") - s.mStatus.SetTitle("Connecting") - s.mUp.Disable() - s.mDown.Enable() - s.mNetworks.Disable() - s.mExitNode.Disable() -} - -func (s *serviceClient) onTrayReady() { - systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) - systray.SetTooltip("NetBird") - - // setup systray menu items - s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected") - s.mStatus.SetIcon(s.icDisconnectedDot) - s.mStatus.Disable() - - profileMenuItem := systray.AddMenuItem("", "") - emailMenuItem := systray.AddMenuItem("", "") - - newProfileMenuArgs := &newProfileMenuArgs{ - ctx: s.ctx, - serviceClient: s, - profileManager: s.profileManager, - eventHandler: s.eventHandler, - profileMenuItem: profileMenuItem, - emailMenuItem: emailMenuItem, - downClickCallback: s.menuDownClick, - upClickCallback: s.menuUpClick, - getSrvClientCallback: s.getSrvClient, - loadSettingsCallback: s.loadSettings, - app: s.app, - } - - s.mProfile = newProfileMenu(*newProfileMenuArgs) - // Seed the transition cache to match the actual default menu - // state (visible / enabled). Without this, the first - // checkAndUpdateFeatures tick that observes DisableProfiles=true - // is a no-op (cache zero-value == desired-false) and the menu - // never gets hidden — symptom: MDM enforces the kill switch but - // the profile menu stays clickable. - s.profilesEnabled = true - - systray.AddSeparator() - s.mUp = systray.AddMenuItem("Connect", "Connect") - s.mDown = systray.AddMenuItem("Disconnect", "Disconnect") - s.mDown.Disable() - systray.AddSeparator() - - s.mSettings = systray.AddMenuItem("Settings", disabledMenuDescr) - s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", allowSSHMenuDescr, false) - s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", autoConnectMenuDescr, false) - s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", quantumResistanceMenuDescr, false) - s.mLazyConnEnabled = s.mSettings.AddSubMenuItemCheckbox("Enable Lazy Connections", lazyConnMenuDescr, false) - s.mBlockInbound = s.mSettings.AddSubMenuItemCheckbox("Block Inbound Connections", blockInboundMenuDescr, false) - s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", notificationsMenuDescr, false) - s.mSettings.AddSeparator() - s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", advancedSettingsMenuDescr) - s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", debugBundleMenuDescr) - s.loadSettings() - - // Disable profile menu if profiles are disabled by daemon. - // DisableUpdateSettings is enforced at the daemon's SetConfig / - // Login gates, not by hiding the UI — so the Settings menu (and - // its Advanced Settings submenu, which has its own kill switch) - // stays visible and the user can still inspect current values. - features, err := s.getFeatures() - if err != nil { - log.Errorf("failed to get features from daemon: %v", err) - // Continue with default behavior if features can't be retrieved - } else if features != nil && features.DisableProfiles { - s.mProfile.setEnabled(false) - s.profilesEnabled = false - } - - s.exitNodeMu.Lock() - s.mExitNode = systray.AddMenuItem("Exit Node", disabledMenuDescr) - s.mExitNode.Disable() - s.exitNodeMu.Unlock() - - s.mNetworks = systray.AddMenuItem("Networks", networksMenuDescr) - s.mNetworks.Disable() - systray.AddSeparator() - - s.mAbout = systray.AddMenuItem("About", "About") - s.mAbout.SetIcon(s.icAbout) - - s.mGitHub = s.mAbout.AddSubMenuItem("GitHub", "GitHub") - - versionString := normalizedVersion(version.NetbirdVersion()) - s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString)) - s.mVersionUI.Disable() - - s.mVersionDaemon = s.mAbout.AddSubMenuItem("", "") - s.mVersionDaemon.Disable() - s.mVersionDaemon.Hide() - - s.mUpdate = s.mAbout.AddSubMenuItem("Download latest version", latestVersionMenuDescr) - s.mUpdate.Hide() - - systray.AddSeparator() - s.mQuit = systray.AddMenuItem("Quit", quitMenuDescr) - - // update exit node menu in case service is already connected - go s.updateExitNodes() - - // Features (DisableProfiles, DisableUpdateSettings, DisableNetworks, - // ...) only change in two ways: at service install time (CLI flag, - // static) and at MDM ticker diff time. The daemon already publishes - // a SystemEvent{type=config_changed} on every MDM-driven engine - // restart, so the UI no longer needs to poll GetFeatures every 2 s. - // A single fetch at startup covers the static CLI-flag case; the - // event handler below covers MDM transitions. updateStatus stays in - // the 2 s loop because connection / peer state genuinely change - // continuously and have no event yet. - s.checkAndUpdateFeatures() - go func() { - s.getSrvConfig() - time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon - for { - err := s.updateStatus() - if err != nil { - log.Errorf("error while updating status: %v", err) - } - - time.Sleep(2 * time.Second) - } - }() - - s.eventManager = event.NewManager(s.notifier, s.addr) - s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) - s.eventManager.AddHandler(func(event *proto.SystemEvent) { - if event.Category == proto.SystemEvent_SYSTEM { - s.updateExitNodes() - } - }) - s.eventManager.AddHandler(func(event *proto.SystemEvent) { - // todo use new Category - if windowAction, ok := event.Metadata["progress_window"]; ok { - targetVersion, ok := event.Metadata["version"] - if !ok { - targetVersion = "unknown" - } - log.Debugf("window action: %v", windowAction) - if windowAction == "show" { - if s.updateContextCancel != nil { - s.updateContextCancel() - s.updateContextCancel = nil - } - - subCtx, cancel := context.WithCancel(s.ctx) - go s.eventHandler.runSelfCommand(subCtx, "update", "--update-version", targetVersion) - s.updateContextCancel = cancel - } - } - }) - s.eventManager.AddHandler(func(event *proto.SystemEvent) { - if newVersion, ok := event.Metadata["new_version_available"]; ok { - _, enforced := event.Metadata["enforced"] - log.Infof("received new_version_available event: version=%s enforced=%v", newVersion, enforced) - s.onUpdateAvailable(newVersion, enforced) - } - }) - s.eventManager.AddHandler(func(event *proto.SystemEvent) { - // Daemon emits a config_changed event after every engine spawn - // (Server.Start, Server.Up, MDM ticker restart). Re-sync the - // tray submenu checkboxes from the fresh daemon-side config so - // the user does not have to restart the tray to see CLI- or - // MDM-driven changes. - if event.Category == proto.SystemEvent_SYSTEM && event.Metadata["type"] == "config_changed" { - log.Infof("config_changed event received (source=%s); refreshing settings + features", event.Metadata["source"]) - s.loadSettings() - // MDM-driven feature kill switches (DisableProfiles / - // DisableUpdateSettings / DisableNetworks) ride the same - // config_changed signal because the daemon re-applies its - // MDM policy on every engine spawn. Pull them in here so - // the UI is up to date without a periodic GetFeatures poll. - s.checkAndUpdateFeatures() - } - }) - - go s.eventManager.Start(s.ctx) - go s.eventHandler.listen(s.ctx) -} - -func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File { - if s.logFile == "" { - // attach child's streams to parent's streams - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - return nil - } - - out, err := os.OpenFile(s.logFile, os.O_WRONLY|os.O_APPEND, 0) - if err != nil { - log.Errorf("Failed to open log file %s: %v", s.logFile, err) - return nil - } - cmd.Stdout = out - cmd.Stderr = out - return out -} - -func normalizedVersion(version string) string { - versionString := version - if unicode.IsDigit(rune(versionString[0])) { - versionString = fmt.Sprintf("v%s", versionString) - } - return versionString -} - -// onTrayExit is called when the tray icon is closed. -func (s *serviceClient) onTrayExit() { - s.cancel() -} - -// getSrvClient connection to the service. -func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) { - s.connLock.Lock() - defer s.connLock.Unlock() - if s.conn != nil { - return s.conn, nil - } - - ctx, cancel := context.WithTimeout(s.ctx, timeout) - defer cancel() - - conn, err := grpc.DialContext( - ctx, - strings.TrimPrefix(s.addr, "tcp://"), - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithBlock(), - grpc.WithUserAgent(desktop.GetUIUserAgent()), - ) - if err != nil { - return nil, fmt.Errorf("dial service: %w", err) - } - - s.conn = proto.NewDaemonServiceClient(conn) - return s.conn, nil -} - -// checkAndUpdateFeatures checks the current features and updates the UI accordingly -func (s *serviceClient) checkAndUpdateFeatures() { - features, err := s.getFeatures() - if err != nil { - log.Errorf("failed to get features from daemon: %v", err) - return - } - - s.updateIndicationLock.Lock() - defer s.updateIndicationLock.Unlock() - - // DisableUpdateSettings is enforced server-side by the daemon gates - // on SetConfig + Login: any attempt to mutate config from UI or - // CLI is rejected at that layer. The UI deliberately keeps the - // Settings menu visible so the user can still inspect current - // values — read-only by virtue of the daemon refusing edits. - - // Update profile menu based on current features - if s.mProfile != nil { - profilesEnabled := features == nil || !features.DisableProfiles - if s.profilesEnabled != profilesEnabled { - s.profilesEnabled = profilesEnabled - s.mProfile.setEnabled(profilesEnabled) - } - } - - // Update networks and exit node menus based on current features. - // `networksEnabled` is the bare feature flag (read elsewhere, e.g. at - // connection-status transitions). `networksMenuEnabled` is the - // transition-cached state actually applied to the menu items — - // it folds in the connection state so a Connected client with the - // kill switch off shows the menus active, and only flips on diff. - s.networksEnabled = features == nil || !features.DisableNetworks - desiredNetworksMenu := s.networksEnabled && s.connected - if desiredNetworksMenu != s.networksMenuEnabled { - s.networksMenuEnabled = desiredNetworksMenu - if desiredNetworksMenu { - s.mNetworks.Enable() - s.mExitNode.Enable() - } else { - s.mNetworks.Disable() - s.mExitNode.Disable() - } - } -} - -// getFeatures from the daemon to determine which features are enabled/disabled. -func (s *serviceClient) getFeatures() (*proto.GetFeaturesResponse, error) { - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - return nil, fmt.Errorf("get client for features: %w", err) - } - - features, err := conn.GetFeatures(s.ctx, &proto.GetFeaturesRequest{}) - if err != nil { - return nil, fmt.Errorf("get features from daemon: %w", err) - } - - return features, nil -} - -// getSrvConfig from the service to show it in the settings window. -func (s *serviceClient) getSrvConfig() { - s.managementURL = profilemanager.DefaultManagementURL - - _, err := s.profileManager.GetActiveProfile() - if err != nil { - log.Errorf("get active profile: %v", err) - return - } - - var cfg *profilemanager.Config - - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - log.Errorf("get client: %v", err) - return - } - - currUser, err := user.Current() - if err != nil { - log.Errorf("get current user: %v", err) - return - } - - activeProf, err := s.profileManager.GetActiveProfile() - if err != nil { - log.Errorf("get active profile: %v", err) - return - } - - srvCfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{ - ProfileName: activeProf.ID.String(), - Username: currUser.Username, - }) - if err != nil { - log.Errorf("get config settings from server: %v", err) - return - } - - cfg = protoConfigToConfig(srvCfg) - - if cfg.ManagementURL.String() != "" { - s.managementURL = cfg.ManagementURL.String() - } - s.preSharedKey = cfg.PreSharedKey - s.RosenpassPermissive = cfg.RosenpassPermissive - s.interfaceName = cfg.WgIface - s.interfacePort = cfg.WgPort - s.mtu = cfg.MTU - - s.networkMonitor = *cfg.NetworkMonitor - s.disableDNS = cfg.DisableDNS - s.disableClientRoutes = cfg.DisableClientRoutes - s.disableServerRoutes = cfg.DisableServerRoutes - s.disableIPv6 = cfg.DisableIPv6 - s.blockLANAccess = cfg.BlockLANAccess - - if cfg.EnableSSHRoot != nil { - s.enableSSHRoot = *cfg.EnableSSHRoot - } - if cfg.EnableSSHSFTP != nil { - s.enableSSHSFTP = *cfg.EnableSSHSFTP - } - if cfg.EnableSSHLocalPortForwarding != nil { - s.enableSSHLocalPortForward = *cfg.EnableSSHLocalPortForwarding - } - if cfg.EnableSSHRemotePortForwarding != nil { - s.enableSSHRemotePortForward = *cfg.EnableSSHRemotePortForwarding - } - if cfg.DisableSSHAuth != nil { - s.disableSSHAuth = *cfg.DisableSSHAuth - } - if cfg.SSHJWTCacheTTL != nil { - s.sshJWTCacheTTL = *cfg.SSHJWTCacheTTL - } - - if s.showAdvancedSettings { - s.iMngURL.SetText(s.managementURL) - // PSK is rendered with an empty Text and a hint via the - // placeholder so the eye toggle never reveals literal asterisks - // (the daemon returns the "**********" sentinel — writing that - // into a PasswordEntry would surface the literal sentinel when - // the user unmasks the field). The placeholder communicates the - // configured / MDM-managed state without exposing any value. - s.iPreSharedKey.SetText("") - s.iPreSharedKey.SetPlaceHolder(preSharedKeyPlaceholder(srvCfg)) - s.iInterfaceName.SetText(cfg.WgIface) - s.iInterfacePort.SetText(strconv.Itoa(cfg.WgPort)) - if cfg.MTU != 0 { - s.iMTU.SetText(strconv.Itoa(int(cfg.MTU))) - } else { - s.iMTU.SetText("") - s.iMTU.SetPlaceHolder(strconv.Itoa(int(iface.DefaultMTU))) - } - s.sRosenpassPermissive.SetChecked(cfg.RosenpassPermissive) - // Re-baseline the enabled state on every refresh: when Rosenpass - // is on the checkbox is editable, when it's off the field is - // inert. Without an explicit Enable() here the control stays - // stuck disabled after a previous refresh (or an MDM unlock) had - // turned it off — applyMDMLocksToSettingsForm below adds the - // MDM lock on top of this baseline. - if cfg.RosenpassEnabled { - s.sRosenpassPermissive.Enable() - } else { - s.sRosenpassPermissive.Disable() - } - s.sNetworkMonitor.SetChecked(*cfg.NetworkMonitor) - s.sDisableDNS.SetChecked(cfg.DisableDNS) - s.sDisableClientRoutes.SetChecked(cfg.DisableClientRoutes) - s.sDisableServerRoutes.SetChecked(cfg.DisableServerRoutes) - s.sDisableIPv6.SetChecked(cfg.DisableIPv6) - s.sBlockLANAccess.SetChecked(cfg.BlockLANAccess) - if cfg.EnableSSHRoot != nil { - s.sEnableSSHRoot.SetChecked(*cfg.EnableSSHRoot) - } - if cfg.EnableSSHSFTP != nil { - s.sEnableSSHSFTP.SetChecked(*cfg.EnableSSHSFTP) - } - if cfg.EnableSSHLocalPortForwarding != nil { - s.sEnableSSHLocalPortForward.SetChecked(*cfg.EnableSSHLocalPortForwarding) - } - if cfg.EnableSSHRemotePortForwarding != nil { - s.sEnableSSHRemotePortForward.SetChecked(*cfg.EnableSSHRemotePortForwarding) - } - if cfg.DisableSSHAuth != nil { - s.sDisableSSHAuth.SetChecked(*cfg.DisableSSHAuth) - } - if cfg.SSHJWTCacheTTL != nil { - s.iSSHJWTCacheTTL.SetText(strconv.Itoa(*cfg.SSHJWTCacheTTL)) - } - } - - // MDM locks must run before the mNotifications-nil early return: - // the Settings window is rendered by a separate UI process launched - // with --settings (see handleAdvancedSettingsClick), and that child - // process does NOT run onReady — so its mNotifications is nil and - // the early return below skipped the lock pass entirely. - s.applyMDMLocks(srvCfg.MDMManagedFields) - - if s.mNotifications == nil { - return - } - if cfg.DisableNotifications != nil && *cfg.DisableNotifications { - s.mNotifications.Uncheck() - } else { - s.mNotifications.Check() - } - if s.eventManager != nil { - s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) - } -} - -func protoConfigToConfig(cfg *proto.GetConfigResponse) *profilemanager.Config { - - var config profilemanager.Config - - if cfg.ManagementUrl != "" { - parsed, err := url.Parse(cfg.ManagementUrl) - if err != nil { - log.Errorf("parse management URL: %v", err) - } else { - config.ManagementURL = parsed - } - } - - if cfg.PreSharedKey != "" { - if cfg.PreSharedKey != censoredPreSharedKey { - config.PreSharedKey = cfg.PreSharedKey - } else { - config.PreSharedKey = "" - } - } - if cfg.AdminURL != "" { - parsed, err := url.Parse(cfg.AdminURL) - if err != nil { - log.Errorf("parse admin URL: %v", err) - } else { - config.AdminURL = parsed - } - } - - config.WgIface = cfg.InterfaceName - if cfg.WireguardPort >= 0 && cfg.WireguardPort <= 65535 { - config.WgPort = int(cfg.WireguardPort) - } else { - config.WgPort = iface.DefaultWgPort - } - - if cfg.Mtu != 0 { - config.MTU = uint16(cfg.Mtu) - } else { - config.MTU = iface.DefaultMTU - } - - config.DisableAutoConnect = cfg.DisableAutoConnect - config.ServerSSHAllowed = &cfg.ServerSSHAllowed - config.RosenpassEnabled = cfg.RosenpassEnabled - config.RosenpassPermissive = cfg.RosenpassPermissive - config.DisableNotifications = &cfg.DisableNotifications - config.LazyConnectionEnabled = cfg.LazyConnectionEnabled - config.BlockInbound = cfg.BlockInbound - config.NetworkMonitor = &cfg.NetworkMonitor - config.DisableDNS = cfg.DisableDns - config.DisableClientRoutes = cfg.DisableClientRoutes - config.DisableServerRoutes = cfg.DisableServerRoutes - config.DisableIPv6 = cfg.DisableIpv6 - config.BlockLANAccess = cfg.BlockLanAccess - - config.EnableSSHRoot = &cfg.EnableSSHRoot - config.EnableSSHSFTP = &cfg.EnableSSHSFTP - config.EnableSSHLocalPortForwarding = &cfg.EnableSSHLocalPortForwarding - config.EnableSSHRemotePortForwarding = &cfg.EnableSSHRemotePortForwarding - config.DisableSSHAuth = &cfg.DisableSSHAuth - - ttl := int(cfg.SshJWTCacheTTL) - config.SSHJWTCacheTTL = &ttl - - return &config -} - -func (s *serviceClient) onUpdateAvailable(newVersion string, enforced bool) { - s.updateIndicationLock.Lock() - defer s.updateIndicationLock.Unlock() - - s.isEnforcedUpdate = enforced - if enforced { - s.mUpdate.SetTitle("Install version " + newVersion) - } else { - s.lastNotifiedVersion = "" - s.mUpdate.SetTitle("Download latest version") - } - - s.mUpdate.Show() - s.isUpdateIconActive = true - - if s.connected { - systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) - } else { - systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) - } - - if enforced && s.lastNotifiedVersion != newVersion { - s.lastNotifiedVersion = newVersion - s.notifier.Send("Update available", "A new version "+newVersion+" is ready to install") - } -} - -// onSessionExpire sends a notification to the user when the session expires. -func (s *serviceClient) onSessionExpire() { - s.sendNotification = true - if s.sendNotification { - go s.eventHandler.runSelfCommand(s.ctx, "login-url", "true") - s.sendNotification = false - } -} - -// loadSettings loads the settings from the config file and updates the UI elements accordingly. -func (s *serviceClient) loadSettings() { - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - log.Errorf("get client: %v", err) - return - } - - currUser, err := user.Current() - if err != nil { - log.Errorf("get current user: %v", err) - return - } - - activeProf, err := s.profileManager.GetActiveProfile() - if err != nil { - log.Errorf("get active profile: %v", err) - return - } - - cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{ - ProfileName: activeProf.ID.String(), - Username: currUser.Username, - }) - if err != nil { - log.Errorf("get config settings from server: %v", err) - return - } - - if cfg.ServerSSHAllowed { - s.mAllowSSH.Check() - } else { - s.mAllowSSH.Uncheck() - } - - if cfg.DisableAutoConnect { - s.mAutoConnect.Uncheck() - } else { - s.mAutoConnect.Check() - } - - if cfg.RosenpassEnabled { - s.mEnableRosenpass.Check() - } else { - s.mEnableRosenpass.Uncheck() - } - - if cfg.LazyConnectionEnabled { - s.mLazyConnEnabled.Check() - } else { - s.mLazyConnEnabled.Uncheck() - } - - if cfg.BlockInbound { - s.mBlockInbound.Check() - } else { - s.mBlockInbound.Uncheck() - } - - if cfg.DisableNotifications { - s.mNotifications.Uncheck() - } else { - s.mNotifications.Check() - } - if s.eventManager != nil { - s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) - } - s.applyMDMLocks(cfg.MDMManagedFields) -} - -// applyMDMLocks disables and badges any tray submenu item or settings- -// form widget whose underlying field is enforced by the active MDM -// policy. Called from loadSettings (submenu refresh) and from -// getSrvConfig (settings-window refresh). Locked items keep their value -// already set by the surrounding refresh code — this routine only -// flips the enabled state and the title suffix, never the value. -func (s *serviceClient) applyMDMLocks(managed []string) { - set := make(map[string]bool, len(managed)) - for _, k := range managed { - set[k] = true - } - s.mdmManagedFields = set - if len(managed) > 0 { - log.Infof("MDM-managed UI fields: %v", managed) - } - - type submenuTarget struct { - item *systray.MenuItem - title string - key string - } - for _, t := range []submenuTarget{ - {s.mAllowSSH, "Allow SSH", mdm.KeyAllowServerSSH}, - {s.mAutoConnect, "Connect on Startup", mdm.KeyDisableAutoConnect}, - {s.mEnableRosenpass, "Enable Quantum-Resistance", mdm.KeyRosenpassEnabled}, - {s.mBlockInbound, "Block Inbound Connections", mdm.KeyBlockInbound}, - } { - if t.item == nil { - continue - } - if set[t.key] { - t.item.SetTitle(t.title + " (MDM)") - t.item.Disable() - } else { - t.item.SetTitle(t.title) - t.item.Enable() - } - } - - s.applyMDMLocksToSettingsForm(set) -} - -// preSharedKeyPlaceholder returns the hint string shown in the PSK -// Entry's placeholder slot. The placeholder is the only signal the -// user gets that a PSK is configured, because the entry's Text is -// forced to empty to keep the password reveal toggle from leaking -// the daemon-returned "**********" redaction sentinel. Returns "" if -// no PSK is present, "MDM-managed" if the key is enforced by MDM, -// and "configured" otherwise. -func preSharedKeyPlaceholder(cfg *proto.GetConfigResponse) string { - if cfg == nil || cfg.PreSharedKey == "" { - return "" - } - for _, k := range cfg.MDMManagedFields { - if k == mdm.KeyPreSharedKey { - return "MDM-managed" - } - } - return "configured" -} - -// applyMDMLocksToSettingsForm disables the per-field input widgets in -// the advanced Settings window when the corresponding MDM key is set. -// For plain-text entries (Management URL, Interface Port) the visible -// value is suffixed with " (MDM)" so the user sees the lock indicator -// inline; for the password entry the suffix is skipped (a password -// widget renders every char as a dot and the indicator would not be -// readable). The widgets are created lazily by showSettingsUI, so -// guard each ref against nil. -func (s *serviceClient) applyMDMLocksToSettingsForm(set map[string]bool) { - type entryTarget struct { - entry *widget.Entry - key string - inlineTag bool - } - for _, t := range []entryTarget{ - {s.iMngURL, mdm.KeyManagementURL, true}, - {s.iPreSharedKey, mdm.KeyPreSharedKey, false}, - {s.iInterfacePort, mdm.KeyWireguardPort, true}, - } { - if t.entry == nil { - continue - } - if set[t.key] { - if t.inlineTag && t.entry.Text != "" && !strings.HasSuffix(t.entry.Text, mdmFieldSuffix) { - t.entry.SetText(t.entry.Text + mdmFieldSuffix) - } - t.entry.Disable() - } else { - if t.inlineTag { - t.entry.SetText(strings.TrimSuffix(t.entry.Text, mdmFieldSuffix)) - } - t.entry.Enable() - } - } - type checkTarget struct { - check *widget.Check - key string - } - for _, t := range []checkTarget{ - {s.sDisableClientRoutes, mdm.KeyDisableClientRoutes}, - {s.sDisableServerRoutes, mdm.KeyDisableServerRoutes}, - } { - if t.check == nil { - continue - } - if set[t.key] { - t.check.Disable() - } else { - t.check.Enable() - } - } - if s.sRosenpassPermissive != nil && set[mdm.KeyRosenpassPermissive] { - // MDM lock layered on top of the Rosenpass-on/off baseline - // applied by getSrvConfig. No Enable() branch here: when the - // MDM key is removed, the next getSrvConfig refresh re-baselines - // the control on cfg.RosenpassEnabled and brings it back if - // Rosenpass is on. - s.sRosenpassPermissive.Disable() - } -} - -// updateConfig updates the configuration parameters -// based on the values selected in the settings window. -func (s *serviceClient) updateConfig() error { - disableAutoStart := !s.mAutoConnect.Checked() - sshAllowed := s.mAllowSSH.Checked() - rosenpassEnabled := s.mEnableRosenpass.Checked() - lazyConnectionEnabled := s.mLazyConnEnabled.Checked() - blockInbound := s.mBlockInbound.Checked() - notificationsDisabled := !s.mNotifications.Checked() - - activeProf, err := s.profileManager.GetActiveProfile() - if err != nil { - log.Errorf("get active profile: %v", err) - return err - } - - currUser, err := user.Current() - if err != nil { - log.Errorf("get current user: %v", err) - return err - } - - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - log.Errorf("get client: %v", err) - return err - } - - req := proto.SetConfigRequest{ - ProfileName: activeProf.ID.String(), - Username: currUser.Username, - DisableAutoConnect: &disableAutoStart, - ServerSSHAllowed: &sshAllowed, - RosenpassEnabled: &rosenpassEnabled, - LazyConnectionEnabled: &lazyConnectionEnabled, - BlockInbound: &blockInbound, - DisableNotifications: ¬ificationsDisabled, - } - - if _, err := conn.SetConfig(s.ctx, &req); err != nil { - log.Errorf("set config settings on server: %v", err) - return err - } - - return nil -} - -// showLoginURL creates a borderless window styled like a pop-up in the top-right corner using s.wLoginURL. -// It also starts a background goroutine that periodically checks if the client is already connected -// and closes the window if so. The goroutine can be cancelled by the returned CancelFunc, and it is -// also cancelled when the window is closed. -func (s *serviceClient) showLoginURL() context.CancelFunc { - - // create a cancellable context for the background check goroutine - ctx, cancel := context.WithCancel(s.ctx) - - resIcon := fyne.NewStaticResource("netbird.png", iconAbout) - - if s.wLoginURL == nil { - s.wLoginURL = s.app.NewWindow("NetBird Session Expired") - s.wLoginURL.Resize(fyne.NewSize(400, 200)) - s.wLoginURL.SetIcon(resIcon) - } - // ensure goroutine is cancelled when the window is closed - s.wLoginURL.SetOnClosed(func() { cancel() }) - // add a description label - label := widget.NewLabel("Your NetBird session has expired.\nPlease re-authenticate to continue using NetBird.") - - btn := widget.NewButtonWithIcon("Re-authenticate", theme.ViewRefreshIcon(), func() { - - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - log.Errorf("get client: %v", err) - return - } - - resp, err := s.login(ctx, false) - if err != nil { - log.Errorf("failed to fetch login URL: %v", err) - return - } - verificationURL := resp.VerificationURIComplete - if verificationURL == "" { - verificationURL = resp.VerificationURI - } - - if verificationURL == "" { - log.Error("no verification URL provided in the login response") - return - } - - if err := openURL(verificationURL); err != nil { - log.Errorf("failed to open login URL: %v", err) - return - } - - _, err = conn.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: resp.UserCode}) - if err != nil { - log.Errorf("Waiting sso login failed with: %v", err) - label.SetText("Waiting login failed, please create \na debug bundle in the settings and contact support.") - return - } - - label.SetText("Re-authentication successful.\nReconnecting") - status, err := conn.Status(ctx, &proto.StatusRequest{}) - if err != nil { - log.Errorf("get service status: %v", err) - return - } - - if status.Status == string(internal.StatusConnected) { - label.SetText("Already connected.\nClosing this window.") - time.Sleep(2 * time.Second) - s.wLoginURL.Close() - return - } - - _, err = conn.Up(ctx, &proto.UpRequest{}) - if err != nil { - label.SetText("Reconnecting failed, please create \na debug bundle in the settings and contact support.") - log.Errorf("Reconnecting failed with: %v", err) - return - } - - label.SetText("Connection successful.\nClosing this window.") - time.Sleep(time.Second) - - s.wLoginURL.Close() - }) - - img := canvas.NewImageFromResource(resIcon) - img.FillMode = canvas.ImageFillContain - img.SetMinSize(fyne.NewSize(64, 64)) - img.Resize(fyne.NewSize(64, 64)) - - // center the content vertically - content := container.NewVBox( - layout.NewSpacer(), - img, - label, - btn, - layout.NewSpacer(), - ) - s.wLoginURL.SetContent(container.NewCenter(content)) - - // start a goroutine to check connection status and close the window if connected - go func() { - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - return - } - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - status, err := conn.Status(s.ctx, &proto.StatusRequest{}) - if err != nil { - continue - } - if status.Status == string(internal.StatusConnected) { - if s.wLoginURL != nil { - s.wLoginURL.Close() - } - return - } - } - } - }() - - s.wLoginURL.Show() - - // return cancel func so callers can stop the background goroutine if desired - return cancel -} - -func openURL(url string) error { - if browser := os.Getenv("BROWSER"); browser != "" { - return exec.Command(browser, url).Start() - } - - var err error - switch runtime.GOOS { - case "windows": - err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - case "darwin": - err = exec.Command("open", url).Start() - case "linux", "freebsd": - err = exec.Command("xdg-open", url).Start() - default: - err = fmt.Errorf("unsupported platform") - } - return err -} diff --git a/client/ui/const.go b/client/ui/const.go deleted file mode 100644 index 48619be75..000000000 --- a/client/ui/const.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -const ( - allowSSHMenuDescr = "Allow SSH connections" - autoConnectMenuDescr = "Connect automatically when the service starts" - quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass" - lazyConnMenuDescr = "[Experimental] Enable lazy connections" - blockInboundMenuDescr = "Block inbound connections to the local machine and routed networks" - notificationsMenuDescr = "Enable notifications" - advancedSettingsMenuDescr = "Advanced settings of the application" - debugBundleMenuDescr = "Create and open debug information bundle" - disabledMenuDescr = "" - networksMenuDescr = "Open the networks management window" - latestVersionMenuDescr = "Download latest version" - quitMenuDescr = "Quit the client app" -) diff --git a/client/ui/debug.go b/client/ui/debug.go deleted file mode 100644 index d3d4fa4f8..000000000 --- a/client/ui/debug.go +++ /dev/null @@ -1,730 +0,0 @@ -//go:build !(linux && 386) - -package main - -import ( - "context" - "fmt" - "path/filepath" - "strconv" - "sync" - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/widget" - log "github.com/sirupsen/logrus" - "github.com/skratchdot/open-golang/open" - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/proto" - uptypes "github.com/netbirdio/netbird/upload-server/types" - "github.com/netbirdio/netbird/version" -) - -// Initial state for the debug collection -type debugInitialState struct { - wasDown bool - needsRestoreUp bool - logLevel proto.LogLevel - isLevelTrace bool -} - -// Debug collection parameters -type debugCollectionParams struct { - duration time.Duration - anonymize bool - systemInfo bool - upload bool - uploadURL string - enablePersistence bool - capture bool -} - -// UI components for progress tracking -type progressUI struct { - statusLabel *widget.Label - progressBar *widget.ProgressBar - uiControls []fyne.Disableable - window fyne.Window -} - -func (s *serviceClient) showDebugUI() { - w := s.app.NewWindow("NetBird Debug") - w.SetOnClosed(s.cancel) - w.Resize(fyne.NewSize(600, 500)) - w.SetFixedSize(true) - - anonymizeCheck := widget.NewCheck("Anonymize sensitive information (public IPs, domains, ...)", nil) - systemInfoCheck := widget.NewCheck("Include system information (routes, interfaces, ...)", nil) - systemInfoCheck.SetChecked(true) - captureCheck := widget.NewCheck("Include packet capture", nil) - uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil) - uploadCheck.SetChecked(true) - - uploadURLContainer, uploadURL := s.buildUploadSection(uploadCheck) - - debugModeContainer, runForDurationCheck, durationInput, noteLabel := s.buildDurationSection() - - statusLabel := widget.NewLabel("") - statusLabel.Hide() - progressBar := widget.NewProgressBar() - progressBar.Hide() - createButton := widget.NewButton("Create Debug Bundle", nil) - - uiControls := []fyne.Disableable{ - anonymizeCheck, systemInfoCheck, captureCheck, - uploadCheck, uploadURL, runForDurationCheck, durationInput, createButton, - } - - createButton.OnTapped = s.getCreateHandler( - statusLabel, progressBar, uploadCheck, uploadURL, - anonymizeCheck, systemInfoCheck, captureCheck, - runForDurationCheck, durationInput, uiControls, w, - ) - - content := container.NewVBox( - widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"), - widget.NewLabel(""), - anonymizeCheck, systemInfoCheck, captureCheck, - uploadCheck, uploadURLContainer, - widget.NewLabel(""), - debugModeContainer, noteLabel, - widget.NewLabel(""), - statusLabel, progressBar, createButton, - ) - - w.SetContent(container.NewPadded(content)) - w.Show() -} - -func (s *serviceClient) buildUploadSection(uploadCheck *widget.Check) (*fyne.Container, *widget.Entry) { - uploadURL := widget.NewEntry() - uploadURL.SetText(uptypes.DefaultBundleURL) - uploadURL.SetPlaceHolder("Enter upload URL") - - uploadURLContainer := container.NewVBox(widget.NewLabel("Debug upload URL:"), uploadURL) - - uploadCheck.OnChanged = func(checked bool) { - if checked { - uploadURLContainer.Show() - } else { - uploadURLContainer.Hide() - } - } - return uploadURLContainer, uploadURL -} - -func (s *serviceClient) buildDurationSection() (*fyne.Container, *widget.Check, *widget.Entry, *widget.Label) { - runForDurationCheck := widget.NewCheck("Run with trace logs before creating bundle", nil) - runForDurationCheck.SetChecked(true) - - forLabel := widget.NewLabel("for") - durationInput := widget.NewEntry() - durationInput.SetText("1") - minutesLabel := widget.NewLabel("minute") - durationInput.Validator = func(s string) error { - return validateMinute(s, minutesLabel) - } - - noteLabel := widget.NewLabel("Note: NetBird will be brought up and down during collection") - - runForDurationCheck.OnChanged = func(checked bool) { - if checked { - forLabel.Show() - durationInput.Show() - minutesLabel.Show() - noteLabel.Show() - } else { - forLabel.Hide() - durationInput.Hide() - minutesLabel.Hide() - noteLabel.Hide() - } - } - - modeContainer := container.NewHBox(runForDurationCheck, forLabel, durationInput, minutesLabel) - return modeContainer, runForDurationCheck, durationInput, noteLabel -} - -func validateMinute(s string, minutesLabel *widget.Label) error { - if val, err := strconv.Atoi(s); err != nil || val < 1 { - return fmt.Errorf("must be a number ≥ 1") - } - if s == "1" { - minutesLabel.SetText("minute") - } else { - minutesLabel.SetText("minutes") - } - return nil -} - -// disableUIControls disables the provided UI controls -func disableUIControls(controls []fyne.Disableable) { - for _, control := range controls { - control.Disable() - } -} - -// enableUIControls enables the provided UI controls -func enableUIControls(controls []fyne.Disableable) { - for _, control := range controls { - control.Enable() - } -} - -func (s *serviceClient) getCreateHandler( - statusLabel *widget.Label, - progressBar *widget.ProgressBar, - uploadCheck *widget.Check, - uploadURL *widget.Entry, - anonymizeCheck *widget.Check, - systemInfoCheck *widget.Check, - captureCheck *widget.Check, - runForDurationCheck *widget.Check, - duration *widget.Entry, - uiControls []fyne.Disableable, - w fyne.Window, -) func() { - return func() { - disableUIControls(uiControls) - statusLabel.Show() - - var url string - if uploadCheck.Checked { - url = uploadURL.Text - if url == "" { - statusLabel.SetText("Error: Upload URL is required when upload is enabled") - enableUIControls(uiControls) - return - } - } - - params := &debugCollectionParams{ - anonymize: anonymizeCheck.Checked, - systemInfo: systemInfoCheck.Checked, - capture: captureCheck.Checked, - upload: uploadCheck.Checked, - uploadURL: url, - enablePersistence: true, - } - - runForDuration := runForDurationCheck.Checked - if runForDuration { - minutes, err := time.ParseDuration(duration.Text + "m") - if err != nil { - statusLabel.SetText(fmt.Sprintf("Error: Invalid duration: %v", err)) - enableUIControls(uiControls) - return - } - params.duration = minutes - - statusLabel.SetText(fmt.Sprintf("Running in debug mode for %d minutes...", int(minutes.Minutes()))) - progressBar.Show() - progressBar.SetValue(0) - - go s.handleRunForDuration( - statusLabel, - progressBar, - uiControls, - w, - params, - ) - return - } - - statusLabel.SetText("Creating debug bundle...") - go s.handleDebugCreation( - params, - statusLabel, - uiControls, - w, - ) - } -} - -func (s *serviceClient) handleRunForDuration( - statusLabel *widget.Label, - progressBar *widget.ProgressBar, - uiControls []fyne.Disableable, - w fyne.Window, - params *debugCollectionParams, -) { - progressUI := &progressUI{ - statusLabel: statusLabel, - progressBar: progressBar, - uiControls: uiControls, - window: w, - } - - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - handleError(progressUI, fmt.Sprintf("Failed to get client for debug: %v", err)) - return - } - - initialState, err := s.getInitialState(conn) - if err != nil { - handleError(progressUI, err.Error()) - return - } - - defer s.restoreServiceState(conn, initialState) - - if err := s.collectDebugData(conn, initialState, params, progressUI); err != nil { - handleError(progressUI, err.Error()) - return - } - - if err := s.createDebugBundleFromCollection(conn, params, progressUI); err != nil { - handleError(progressUI, err.Error()) - return - } - - progressUI.statusLabel.SetText("Bundle created successfully") -} - -// Get initial state of the service -func (s *serviceClient) getInitialState(conn proto.DaemonServiceClient) (*debugInitialState, error) { - statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{}) - if err != nil { - return nil, fmt.Errorf(" get status: %v", err) - } - - logLevelResp, err := conn.GetLogLevel(s.ctx, &proto.GetLogLevelRequest{}) - if err != nil { - return nil, fmt.Errorf("get log level: %v", err) - } - - wasDown := statusResp.Status != string(internal.StatusConnected) && - statusResp.Status != string(internal.StatusConnecting) - - initialLogLevel := logLevelResp.GetLevel() - initialLevelTrace := initialLogLevel >= proto.LogLevel_TRACE - - return &debugInitialState{ - wasDown: wasDown, - logLevel: initialLogLevel, - isLevelTrace: initialLevelTrace, - }, nil -} - -// Handle progress tracking during collection -func startProgressTracker(ctx context.Context, wg *sync.WaitGroup, duration time.Duration, progress *progressUI) { - progress.progressBar.Show() - progress.progressBar.SetValue(0) - - startTime := time.Now() - endTime := startTime.Add(duration) - wg.Add(1) - - go func() { - defer wg.Done() - ticker := time.NewTicker(500 * time.Millisecond) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - remaining := time.Until(endTime) - if remaining <= 0 { - remaining = 0 - } - - elapsed := time.Since(startTime) - progressVal := float64(elapsed) / float64(duration) - if progressVal > 1.0 { - progressVal = 1.0 - } - - progress.progressBar.SetValue(progressVal) - progress.statusLabel.SetText(fmt.Sprintf("Running with trace logs... %s remaining", formatDuration(remaining))) - } - } - }() - -} - -func (s *serviceClient) configureServiceForDebug( - conn proto.DaemonServiceClient, - state *debugInitialState, - params *debugCollectionParams, -) { - if state.wasDown { - if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil { - log.Warnf("failed to bring service up: %v", err) - } else { - log.Info("Service brought up for debug") - time.Sleep(time.Second * 10) - } - } - - if !state.isLevelTrace { - if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel_TRACE}); err != nil { - log.Warnf("failed to set log level to TRACE: %v", err) - } else { - log.Info("Log level set to TRACE for debug") - } - } - - if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil { - log.Warnf("failed to bring service down: %v", err) - } else { - state.needsRestoreUp = !state.wasDown - time.Sleep(time.Second) - } - - if params.enablePersistence { - if _, err := conn.SetSyncResponsePersistence(s.ctx, &proto.SetSyncResponsePersistenceRequest{ - Enabled: true, - }); err != nil { - log.Warnf("failed to enable sync response persistence: %v", err) - } else { - log.Info("Sync response persistence enabled for debug") - } - } - - if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil { - log.Warnf("failed to bring service back up: %v", err) - } else { - state.needsRestoreUp = false - time.Sleep(time.Second * 3) - } - - if _, err := conn.StartCPUProfile(s.ctx, &proto.StartCPUProfileRequest{}); err != nil { - log.Warnf("failed to start CPU profiling: %v", err) - } - - s.startBundleCaptureIfEnabled(conn, params) -} - -func (s *serviceClient) startBundleCaptureIfEnabled(conn proto.DaemonServiceClient, params *debugCollectionParams) { - if !params.capture { - return - } - - const maxCapture = 10 * time.Minute - timeout := params.duration + 30*time.Second - if timeout > maxCapture { - timeout = maxCapture - log.Warnf("packet capture clamped to %s (server maximum)", maxCapture) - } - if _, err := conn.StartBundleCapture(s.ctx, &proto.StartBundleCaptureRequest{ - Timeout: durationpb.New(timeout), - }); err != nil { - log.Warnf("failed to start bundle capture: %v", err) - } -} - -func (s *serviceClient) collectDebugData( - conn proto.DaemonServiceClient, - state *debugInitialState, - params *debugCollectionParams, - progress *progressUI, -) error { - ctx, cancel := context.WithTimeout(s.ctx, params.duration) - defer cancel() - var wg sync.WaitGroup - startProgressTracker(ctx, &wg, params.duration, progress) - - s.configureServiceForDebug(conn, state, params) - - wg.Wait() - progress.progressBar.Hide() - progress.statusLabel.SetText("Collecting debug data...") - - if _, err := conn.StopCPUProfile(s.ctx, &proto.StopCPUProfileRequest{}); err != nil { - log.Warnf("failed to stop CPU profiling: %v", err) - } - - if params.capture { - stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if _, err := conn.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil { - log.Warnf("failed to stop bundle capture: %v", err) - } - } - - return nil -} - -// Create the debug bundle with collected data -func (s *serviceClient) createDebugBundleFromCollection( - conn proto.DaemonServiceClient, - params *debugCollectionParams, - progress *progressUI, -) error { - progress.statusLabel.SetText("Creating debug bundle with collected logs...") - - request := &proto.DebugBundleRequest{ - Anonymize: params.anonymize, - SystemInfo: params.systemInfo, - CliVersion: version.NetbirdVersion(), - } - - if params.upload { - request.UploadURL = params.uploadURL - } - - resp, err := conn.DebugBundle(s.ctx, request) - if err != nil { - return fmt.Errorf("create debug bundle: %v", err) - } - - // Show appropriate dialog based on upload status - localPath := resp.GetPath() - uploadFailureReason := resp.GetUploadFailureReason() - uploadedKey := resp.GetUploadedKey() - - if params.upload { - if uploadFailureReason != "" { - showUploadFailedDialog(progress.window, localPath, uploadFailureReason) - } else { - showUploadSuccessDialog(s.app, progress.window, localPath, uploadedKey) - } - } else { - showBundleCreatedDialog(progress.window, localPath) - } - - enableUIControls(progress.uiControls) - return nil -} - -// Restore service to original state -func (s *serviceClient) restoreServiceState(conn proto.DaemonServiceClient, state *debugInitialState) { - if state.needsRestoreUp { - if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil { - log.Warnf("failed to restore up state: %v", err) - } else { - log.Info("Service state restored to up") - } - } - - if state.wasDown { - if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil { - log.Warnf("failed to restore down state: %v", err) - } else { - log.Info("Service state restored to down") - } - } - - if !state.isLevelTrace { - if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: state.logLevel}); err != nil { - log.Warnf("failed to restore log level: %v", err) - } else { - log.Info("Log level restored to original setting") - } - } -} - -// Handle errors during debug collection -func handleError(progress *progressUI, errMsg string) { - log.Errorf("%s", errMsg) - progress.statusLabel.SetText(errMsg) - progress.progressBar.Hide() - enableUIControls(progress.uiControls) -} - -func (s *serviceClient) handleDebugCreation( - params *debugCollectionParams, - statusLabel *widget.Label, - uiControls []fyne.Disableable, - w fyne.Window, -) { - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - log.Errorf("Failed to get client for debug: %v", err) - statusLabel.SetText(fmt.Sprintf("Error: %v", err)) - enableUIControls(uiControls) - return - } - - if params.capture { - if _, err := conn.StartBundleCapture(s.ctx, &proto.StartBundleCaptureRequest{ - Timeout: durationpb.New(30 * time.Second), - }); err != nil { - log.Warnf("failed to start bundle capture: %v", err) - } else { - defer func() { - stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if _, err := conn.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil { - log.Warnf("failed to stop bundle capture: %v", err) - } - }() - time.Sleep(2 * time.Second) - } - } - - resp, err := s.createDebugBundle(params.anonymize, params.systemInfo, params.uploadURL) - if err != nil { - log.Errorf("Failed to create debug bundle: %v", err) - statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err)) - enableUIControls(uiControls) - return - } - - localPath := resp.GetPath() - uploadFailureReason := resp.GetUploadFailureReason() - uploadedKey := resp.GetUploadedKey() - - if params.upload { - if uploadFailureReason != "" { - showUploadFailedDialog(w, localPath, uploadFailureReason) - } else { - showUploadSuccessDialog(s.app, w, localPath, uploadedKey) - } - } else { - showBundleCreatedDialog(w, localPath) - } - - enableUIControls(uiControls) - statusLabel.SetText("Bundle created successfully") -} - -func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploadURL string) (*proto.DebugBundleResponse, error) { - conn, err := s.getSrvClient(failFastTimeout) - if err != nil { - return nil, fmt.Errorf("get client: %v", err) - } - - request := &proto.DebugBundleRequest{ - Anonymize: anonymize, - SystemInfo: systemInfo, - CliVersion: version.NetbirdVersion(), - } - - if uploadURL != "" { - request.UploadURL = uploadURL - } - - resp, err := conn.DebugBundle(s.ctx, request) - if err != nil { - return nil, fmt.Errorf("failed to create debug bundle via daemon: %v", err) - } - - return resp, nil -} - -// formatDuration formats a duration in HH:MM:SS format -func formatDuration(d time.Duration) string { - d = d.Round(time.Second) - h := d / time.Hour - d %= time.Hour - m := d / time.Minute - d %= time.Minute - s := d / time.Second - return fmt.Sprintf("%02d:%02d:%02d", h, m, s) -} - -// createButtonWithAction creates a button with the given label and action -func createButtonWithAction(label string, action func()) *widget.Button { - button := widget.NewButton(label, action) - return button -} - -// showUploadFailedDialog displays a dialog when upload fails -func showUploadFailedDialog(w fyne.Window, localPath, failureReason string) { - content := container.NewVBox( - widget.NewLabel(fmt.Sprintf("Bundle upload failed:\n%s\n\n"+ - "A local copy was saved at:\n%s", failureReason, localPath)), - ) - - customDialog := dialog.NewCustom("Upload Failed", "Cancel", content, w) - - buttonBox := container.NewHBox( - createButtonWithAction("Open file", func() { - log.Infof("Attempting to open local file: %s", localPath) - if openErr := open.Start(localPath); openErr != nil { - log.Errorf("Failed to open local file '%s': %v", localPath, openErr) - dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w) - } - }), - createButtonWithAction("Open folder", func() { - folderPath := filepath.Dir(localPath) - log.Infof("Attempting to open local folder: %s", folderPath) - if openErr := open.Start(folderPath); openErr != nil { - log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr) - dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w) - } - }), - ) - - content.Add(buttonBox) - customDialog.Show() -} - -// showUploadSuccessDialog displays a dialog when upload succeeds -func showUploadSuccessDialog(a fyne.App, w fyne.Window, localPath, uploadedKey string) { - log.Infof("Upload key: %s", uploadedKey) - keyEntry := widget.NewEntry() - keyEntry.SetText(uploadedKey) - keyEntry.Disable() - - content := container.NewVBox( - widget.NewLabel("Bundle uploaded successfully!"), - widget.NewLabel(""), - widget.NewLabel("Upload key:"), - keyEntry, - widget.NewLabel(""), - widget.NewLabel(fmt.Sprintf("Local copy saved at:\n%s", localPath)), - ) - - customDialog := dialog.NewCustom("Upload Successful", "OK", content, w) - - copyBtn := createButtonWithAction("Copy key", func() { - a.Clipboard().SetContent(uploadedKey) - log.Info("Upload key copied to clipboard") - }) - - buttonBox := createButtonBox(localPath, w, copyBtn) - content.Add(buttonBox) - customDialog.Show() -} - -// showBundleCreatedDialog displays a dialog when bundle is created without upload -func showBundleCreatedDialog(w fyne.Window, localPath string) { - content := container.NewVBox( - widget.NewLabel(fmt.Sprintf("Bundle created locally at:\n%s\n\n"+ - "Administrator privileges may be required to access the file.", localPath)), - ) - - customDialog := dialog.NewCustom("Debug Bundle Created", "Cancel", content, w) - - buttonBox := createButtonBox(localPath, w, nil) - content.Add(buttonBox) - customDialog.Show() -} - -func createButtonBox(localPath string, w fyne.Window, elems ...fyne.Widget) *fyne.Container { - box := container.NewHBox() - for _, elem := range elems { - box.Add(elem) - } - - fileBtn := createButtonWithAction("Open file", func() { - log.Infof("Attempting to open local file: %s", localPath) - if openErr := open.Start(localPath); openErr != nil { - log.Errorf("Failed to open local file '%s': %v", localPath, openErr) - dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w) - } - }) - - folderBtn := createButtonWithAction("Open folder", func() { - folderPath := filepath.Dir(localPath) - log.Infof("Attempting to open local folder: %s", folderPath) - if openErr := open.Start(folderPath); openErr != nil { - log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr) - dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w) - } - }) - - box.Add(fileBtn) - box.Add(folderBtn) - - return box -} diff --git a/client/ui/dock_darwin.go b/client/ui/dock_darwin.go new file mode 100644 index 000000000..dd8c60073 --- /dev/null +++ b/client/ui/dock_darwin.go @@ -0,0 +1,70 @@ +//go:build darwin + +package main + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Cocoa +#import + +static int lastDockState = -1; + +static void refreshDockPolicy(void) { + dispatch_async(dispatch_get_main_queue(), ^{ + Class cls = NSClassFromString(@"WebviewWindow"); + if (cls == nil) { + return; + } + int visible = 0; + for (NSWindow *w in [NSApp windows]) { + if ([w isKindOfClass:cls] && [w isVisible]) { + visible = 1; + break; + } + } + if (visible == lastDockState) { + return; + } + lastDockState = visible; + + // Set application to "Regular" and show dock icon (when visible) or to "Accessory" (when hidden) + if (visible) { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp activateIgnoringOtherApps:YES]; + } else { + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; + } + }); +} + +static int dockObserverInstalled = 0; + +static void initDockObserver(void) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (dockObserverInstalled) { + return; + } + dockObserverInstalled = 1; + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + void (^trigger)(NSNotification *) = ^(NSNotification *_) { + refreshDockPolicy(); + }; + + [nc addObserverForName:NSWindowDidChangeOcclusionStateNotification + object:nil + queue:nil + usingBlock:trigger]; + [nc addObserverForName:NSWindowWillCloseNotification + object:nil + queue:nil + usingBlock:trigger]; + + refreshDockPolicy(); + }); +} +*/ +import "C" + +func initDockObserver() { + C.initDockObserver() +} diff --git a/client/ui/dock_other.go b/client/ui/dock_other.go new file mode 100644 index 000000000..0ace89552 --- /dev/null +++ b/client/ui/dock_other.go @@ -0,0 +1,7 @@ +//go:build !darwin && !android && !ios && !freebsd && !js + +package main + +func initDockObserver() { + // macOS-only; Linux and Windows taskbar entries already gate on window visibility natively. +} diff --git a/client/ui/event/event.go b/client/ui/event/event.go deleted file mode 100644 index 3b43fdc7f..000000000 --- a/client/ui/event/event.go +++ /dev/null @@ -1,184 +0,0 @@ -package event - -import ( - "context" - "fmt" - "slices" - "strings" - "sync" - "time" - - "github.com/cenkalti/backoff/v4" - log "github.com/sirupsen/logrus" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - - "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/client/ui/desktop" -) - -// Notifier sends desktop notifications. Defined here so the event package -// does not depend on fyne or the platform-specific notifier implementation. -type Notifier interface { - Send(title, body string) -} - -type Handler func(*proto.SystemEvent) - -type Manager struct { - notifier Notifier - addr string - - mu sync.Mutex - ctx context.Context - cancel context.CancelFunc - enabled bool - handlers []Handler -} - -func NewManager(notifier Notifier, addr string) *Manager { - return &Manager{ - notifier: notifier, - addr: addr, - } -} - -func (e *Manager) Start(ctx context.Context) { - e.mu.Lock() - e.ctx, e.cancel = context.WithCancel(ctx) - e.mu.Unlock() - - expBackOff := backoff.WithContext(&backoff.ExponentialBackOff{ - InitialInterval: time.Second, - RandomizationFactor: backoff.DefaultRandomizationFactor, - Multiplier: backoff.DefaultMultiplier, - MaxInterval: 10 * time.Second, - MaxElapsedTime: 0, - Stop: backoff.Stop, - Clock: backoff.SystemClock, - }, ctx) - - if err := backoff.Retry(e.streamEvents, expBackOff); err != nil { - log.Errorf("event stream ended: %v", err) - } -} - -func (e *Manager) streamEvents() error { - e.mu.Lock() - ctx := e.ctx - e.mu.Unlock() - - client, err := getClient(e.addr) - if err != nil { - return fmt.Errorf("create client: %w", err) - } - - stream, err := client.SubscribeEvents(ctx, &proto.SubscribeRequest{}) - if err != nil { - return fmt.Errorf("failed to subscribe to events: %w", err) - } - - log.Info("subscribed to daemon events") - defer func() { - log.Info("unsubscribed from daemon events") - }() - - for { - event, err := stream.Recv() - if err != nil { - return fmt.Errorf("error receiving event: %w", err) - } - e.handleEvent(event) - } -} - -func (e *Manager) Stop() { - e.mu.Lock() - defer e.mu.Unlock() - if e.cancel != nil { - e.cancel() - } -} - -func (e *Manager) SetNotificationsEnabled(enabled bool) { - e.mu.Lock() - defer e.mu.Unlock() - e.enabled = enabled -} - -func (e *Manager) handleEvent(event *proto.SystemEvent) { - e.mu.Lock() - enabled := e.enabled - handlers := slices.Clone(e.handlers) - e.mu.Unlock() - - if event.UserMessage != "" && (enabled || event.Severity == proto.SystemEvent_CRITICAL) && !isV6DefaultRoutePartner(event) { - title := e.getEventTitle(event) - body := event.UserMessage - id := event.Metadata["id"] - if id != "" { - body += fmt.Sprintf(" ID: %s", id) - } - e.notifier.Send(title, body) - } - - for _, handler := range handlers { - go handler(event) - } -} - -func (e *Manager) AddHandler(handler Handler) { - e.mu.Lock() - defer e.mu.Unlock() - e.handlers = append(e.handlers, handler) -} - -// isV6DefaultRoutePartner reports whether the event is the IPv6 half of a -// paired v4/v6 default-route event. Management always pairs ::/0 with 0.0.0.0/0 -// for exit nodes, so the v4 partner already drives the user-facing toast and -// the v6 one is suppressed to avoid a duplicate notification. -func isV6DefaultRoutePartner(event *proto.SystemEvent) bool { - return event.Category == proto.SystemEvent_NETWORK && event.Metadata["network"] == "::/0" -} - -func (e *Manager) getEventTitle(event *proto.SystemEvent) string { - var prefix string - switch event.Severity { - case proto.SystemEvent_CRITICAL: - prefix = "Critical" - case proto.SystemEvent_ERROR: - prefix = "Error" - case proto.SystemEvent_WARNING: - prefix = "Warning" - default: - prefix = "Info" - } - - var category string - switch event.Category { - case proto.SystemEvent_DNS: - category = "DNS" - case proto.SystemEvent_NETWORK: - category = "Network" - case proto.SystemEvent_AUTHENTICATION: - category = "Authentication" - case proto.SystemEvent_CONNECTIVITY: - category = "Connectivity" - default: - category = "System" - } - - return fmt.Sprintf("%s: %s", prefix, category) -} - -func getClient(addr string) (proto.DaemonServiceClient, error) { - conn, err := grpc.NewClient( - strings.TrimPrefix(addr, "tcp://"), - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithUserAgent(desktop.GetUIUserAgent()), - ) - if err != nil { - return nil, err - } - return proto.NewDaemonServiceClient(conn), nil -} diff --git a/client/ui/event_handler.go b/client/ui/event_handler.go deleted file mode 100644 index 876fcef5f..000000000 --- a/client/ui/event_handler.go +++ /dev/null @@ -1,326 +0,0 @@ -//go:build !(linux && 386) - -package main - -import ( - "context" - "errors" - "fmt" - "os" - "os/exec" - - "fyne.io/systray" - log "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/version" -) - -type eventHandler struct { - client *serviceClient -} - -func newEventHandler(client *serviceClient) *eventHandler { - return &eventHandler{ - client: client, - } -} - -func (h *eventHandler) listen(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case <-h.client.mUp.ClickedCh: - h.handleConnectClick() - case <-h.client.mDown.ClickedCh: - h.handleDisconnectClick() - case <-h.client.mAllowSSH.ClickedCh: - h.handleAllowSSHClick() - case <-h.client.mAutoConnect.ClickedCh: - h.handleAutoConnectClick() - case <-h.client.mEnableRosenpass.ClickedCh: - h.handleRosenpassClick() - case <-h.client.mLazyConnEnabled.ClickedCh: - h.handleLazyConnectionClick() - case <-h.client.mBlockInbound.ClickedCh: - h.handleBlockInboundClick() - case <-h.client.mAdvancedSettings.ClickedCh: - h.handleAdvancedSettingsClick() - case <-h.client.mCreateDebugBundle.ClickedCh: - h.handleCreateDebugBundleClick() - case <-h.client.mQuit.ClickedCh: - h.handleQuitClick() - return - case <-h.client.mGitHub.ClickedCh: - h.handleGitHubClick() - case <-h.client.mUpdate.ClickedCh: - h.handleUpdateClick() - case <-h.client.mNetworks.ClickedCh: - h.handleNetworksClick() - case <-h.client.mNotifications.ClickedCh: - h.handleNotificationsClick() - case <-systray.TrayOpenedCh: - h.client.updateExitNodes() - } - } -} - -func (h *eventHandler) handleConnectClick() { - h.client.mUp.Disable() - - if h.client.connectCancel != nil { - h.client.connectCancel() - } - - connectCtx, connectCancel := context.WithCancel(h.client.ctx) - h.client.connectCancel = connectCancel - - go func() { - defer connectCancel() - - if err := h.client.menuUpClick(connectCtx); err != nil { - st, ok := status.FromError(err) - if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) { - log.Debugf("connect operation cancelled by user") - } else { - h.client.notifier.Send("Error", "Failed to connect") - log.Errorf("connect failed: %v", err) - } - } - - if err := h.client.updateStatus(); err != nil { - log.Debugf("failed to update status after connect: %v", err) - } - }() -} - -func (h *eventHandler) handleDisconnectClick() { - h.client.mDown.Disable() - h.client.cancelExitNodeRetry() - - if h.client.connectCancel != nil { - log.Debugf("cancelling ongoing connect operation") - h.client.connectCancel() - h.client.connectCancel = nil - } - - go func() { - if err := h.client.menuDownClick(); err != nil { - st, ok := status.FromError(err) - if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) { - h.client.notifier.Send("Error", "Failed to disconnect") - log.Errorf("disconnect failed: %v", err) - } else { - log.Debugf("disconnect cancelled or already disconnecting") - } - } - - if err := h.client.updateStatus(); err != nil { - log.Debugf("failed to update status after disconnect: %v", err) - } - }() -} - -func (h *eventHandler) handleAllowSSHClick() { - h.toggleCheckbox(h.client.mAllowSSH) - if err := h.updateConfigWithErr(); err != nil { - h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error - log.Errorf("failed to update config: %v", err) - h.client.notifier.Send("Error", "Failed to update SSH settings") - } - -} - -func (h *eventHandler) handleAutoConnectClick() { - h.toggleCheckbox(h.client.mAutoConnect) - if err := h.updateConfigWithErr(); err != nil { - h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error - log.Errorf("failed to update config: %v", err) - h.client.notifier.Send("Error", "Failed to update auto-connect settings") - } -} - -func (h *eventHandler) handleRosenpassClick() { - h.toggleCheckbox(h.client.mEnableRosenpass) - if err := h.updateConfigWithErr(); err != nil { - h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error - log.Errorf("failed to update config: %v", err) - h.client.notifier.Send("Error", "Failed to update Rosenpass settings") - } -} - -func (h *eventHandler) handleLazyConnectionClick() { - h.toggleCheckbox(h.client.mLazyConnEnabled) - if err := h.updateConfigWithErr(); err != nil { - h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error - log.Errorf("failed to update config: %v", err) - h.client.notifier.Send("Error", "Failed to update lazy connection settings") - } -} - -func (h *eventHandler) handleBlockInboundClick() { - h.toggleCheckbox(h.client.mBlockInbound) - if err := h.updateConfigWithErr(); err != nil { - h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error - log.Errorf("failed to update config: %v", err) - h.client.notifier.Send("Error", "Failed to update block inbound settings") - } -} - -func (h *eventHandler) handleNotificationsClick() { - h.toggleCheckbox(h.client.mNotifications) - if err := h.updateConfigWithErr(); err != nil { - h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error - log.Errorf("failed to update config: %v", err) - h.client.notifier.Send("Error", "Failed to update notifications settings") - } else if h.client.eventManager != nil { - h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked()) - } - -} - -func (h *eventHandler) handleAdvancedSettingsClick() { - h.client.mAdvancedSettings.Disable() - go func() { - defer h.client.mAdvancedSettings.Enable() - defer h.client.getSrvConfig() - h.runSelfCommand(h.client.ctx, "settings") - }() -} - -func (h *eventHandler) handleCreateDebugBundleClick() { - h.client.mCreateDebugBundle.Disable() - go func() { - defer h.client.mCreateDebugBundle.Enable() - h.runSelfCommand(h.client.ctx, "debug") - }() -} - -func (h *eventHandler) handleQuitClick() { - systray.Quit() -} - -func (h *eventHandler) handleGitHubClick() { - if err := openURL("https://github.com/netbirdio/netbird"); err != nil { - log.Errorf("failed to open GitHub URL: %v", err) - } -} - -func (h *eventHandler) handleUpdateClick() { - h.client.updateIndicationLock.Lock() - enforced := h.client.isEnforcedUpdate - h.client.updateIndicationLock.Unlock() - - if !enforced { - if err := openURL(version.DownloadUrl()); err != nil { - log.Errorf("failed to open download URL: %v", err) - } - return - } - - // prevent blocking against a busy server - h.client.mUpdate.Disable() - go func() { - defer h.client.mUpdate.Enable() - conn, err := h.client.getSrvClient(defaultFailTimeout) - if err != nil { - log.Errorf("failed to get service client for update: %v", err) - _ = openURL(version.DownloadUrl()) - return - } - - resp, err := conn.TriggerUpdate(h.client.ctx, &proto.TriggerUpdateRequest{}) - if err != nil { - log.Errorf("TriggerUpdate failed: %v", err) - _ = openURL(version.DownloadUrl()) - return - } - if !resp.Success { - log.Errorf("TriggerUpdate failed: %s", resp.ErrorMsg) - _ = openURL(version.DownloadUrl()) - return - } - - log.Infof("update triggered via daemon") - }() -} - -func (h *eventHandler) handleNetworksClick() { - h.client.mNetworks.Disable() - go func() { - defer h.client.mNetworks.Enable() - h.runSelfCommand(h.client.ctx, "networks") - }() -} - -func (h *eventHandler) toggleCheckbox(item *systray.MenuItem) { - if item.Checked() { - item.Uncheck() - } else { - item.Check() - } -} - -func (h *eventHandler) updateConfigWithErr() error { - if err := h.client.updateConfig(); err != nil { - return err - } - - return nil -} - -func (h *eventHandler) runSelfCommand(ctx context.Context, command string, args ...string) { - proc, err := os.Executable() - if err != nil { - log.Errorf("error getting executable path: %v", err) - return - } - - // Build the full command arguments - cmdArgs := []string{ - fmt.Sprintf("--%s=true", command), - fmt.Sprintf("--daemon-addr=%s", h.client.addr), - } - cmdArgs = append(cmdArgs, args...) - - cmd := exec.CommandContext(ctx, proc, cmdArgs...) - - if out := h.client.attachOutput(cmd); out != nil { - defer func() { - if err := out.Close(); err != nil { - log.Errorf("error closing log file %s: %v", h.client.logFile, err) - } - }() - } - - log.Printf("running command: %s", cmd.String()) - - if err := cmd.Run(); err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - log.Printf("command '%s' failed with exit code %d", cmd.String(), exitErr.ExitCode()) - } - return - } - - log.Printf("command '%s' completed successfully", cmd.String()) -} - -func (h *eventHandler) logout(ctx context.Context) error { - client, err := h.client.getSrvClient(defaultFailTimeout) - if err != nil { - return fmt.Errorf("failed to get service client: %w", err) - } - - _, err = client.Logout(ctx, &proto.LogoutRequest{}) - if err != nil { - return fmt.Errorf("logout failed: %w", err) - } - - h.client.getSrvConfig() - - return nil -} diff --git a/client/ui/font_bsd.go b/client/ui/font_bsd.go deleted file mode 100644 index 139f38f40..000000000 --- a/client/ui/font_bsd.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build freebsd || openbsd || netbsd || dragonfly - -package main - -import ( - "os" - "runtime" - - log "github.com/sirupsen/logrus" -) - -func (s *serviceClient) setDefaultFonts() { - paths := []string{ - "/usr/local/share/fonts/TTF/DejaVuSans.ttf", - "/usr/local/share/fonts/dejavu/DejaVuSans.ttf", - "/usr/local/share/noto/NotoSans-Regular.ttf", - "/usr/local/share/fonts/noto/NotoSans-Regular.ttf", - "/usr/local/share/fonts/liberation-fonts-ttf/LiberationSans-Regular.ttf", - } - - for _, fontPath := range paths { - if _, err := os.Stat(fontPath); err == nil { - os.Setenv("FYNE_FONT", fontPath) - log.Debugf("Using font: %s", fontPath) - return - } - } - - log.Errorf("Failed to find any suitable font files for %s", runtime.GOOS) -} diff --git a/client/ui/font_darwin.go b/client/ui/font_darwin.go deleted file mode 100644 index cafb72f59..000000000 --- a/client/ui/font_darwin.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "os" - - log "github.com/sirupsen/logrus" -) - -const defaultFontPath = "/Library/Fonts/Arial Unicode.ttf" - -func (s *serviceClient) setDefaultFonts() { - if _, err := os.Stat(defaultFontPath); err != nil { - log.Errorf("Failed to find default font file: %v", err) - return - } - - os.Setenv("FYNE_FONT", defaultFontPath) -} diff --git a/client/ui/font_linux.go b/client/ui/font_linux.go deleted file mode 100644 index 4aa92494a..000000000 --- a/client/ui/font_linux.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !386 - -package main - -func (s *serviceClient) setDefaultFonts() { - //TODO: Linux Multiple Language Support -} diff --git a/client/ui/font_windows.go b/client/ui/font_windows.go deleted file mode 100644 index 6346a9fb9..000000000 --- a/client/ui/font_windows.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "os" - "path" - "unsafe" - - log "github.com/sirupsen/logrus" - "golang.org/x/sys/windows" -) - -func (s *serviceClient) setDefaultFonts() { - defaultFontPath := s.getWindowsFontFilePath() - - if _, err := os.Stat(defaultFontPath); err != nil { - log.Errorf("Failed to find default font file: %v", err) - return - } - - os.Setenv("FYNE_FONT", defaultFontPath) -} - -func (s *serviceClient) getWindowsFontFilePath() string { - var ( - fontFolder = "C:/Windows/Fonts" - fontMapping = map[string]string{ - "default": "Segoeui.ttf", - "zh-CN": "Segoeui.ttf", - "am-ET": "Ebrima.ttf", - "nirmala": "Nirmala.ttf", - "chr-CHER-US": "Gadugi.ttf", - "zh-HK": "Segoeui.ttf", - "zh-TW": "Segoeui.ttf", - "km-KH": "Leelawui.ttf", - "ko-KR": "Malgun.ttf", - "th-TH": "Leelawui.ttf", - "ti-ET": "Ebrima.ttf", - } - nirMalaLang = []string{ - "as-IN", - "bn-BD", - "bn-IN", - "gu-IN", - "hi-IN", - "kn-IN", - "kok-IN", - "ml-IN", - "mr-IN", - "ne-NP", - "or-IN", - "pa-IN", - "si-LK", - "ta-IN", - "te-IN", - } - ) - - // getUserDefaultLocaleName.Call() panics if the func is not found - defer func() { - if r := recover(); r != nil { - log.Errorf("Recovered from panic: %v", r) - } - }() - - kernel32 := windows.NewLazySystemDLL("kernel32.dll") - getUserDefaultLocaleName := kernel32.NewProc("GetUserDefaultLocaleName") - - buf := make([]uint16, 85) // LOCALE_NAME_MAX_LENGTH is usually 85 - r, _, err := getUserDefaultLocaleName.Call(uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) - // returns 0 on failure, err is always non-nil - // https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultlocalename - if r == 0 { - log.Errorf("GetUserDefaultLocaleName call failed: %v", err) - return path.Join(fontFolder, fontMapping["default"]) - } - - defaultLanguage := windows.UTF16ToString(buf) - - for _, lang := range nirMalaLang { - if defaultLanguage == lang { - return path.Join(fontFolder, fontMapping["nirmala"]) - } - } - - if font, ok := fontMapping[defaultLanguage]; ok { - return path.Join(fontFolder, font) - } - - return path.Join(fontFolder, fontMapping["default"]) -} diff --git a/client/ui/frontend/.prettierignore b/client/ui/frontend/.prettierignore new file mode 100644 index 000000000..c78cb7cc3 --- /dev/null +++ b/client/ui/frontend/.prettierignore @@ -0,0 +1,7 @@ +dist +build +node_modules +pnpm-lock.yaml +wailsjs +*.min.js +*.min.css diff --git a/client/ui/frontend/.prettierrc b/client/ui/frontend/.prettierrc new file mode 100644 index 000000000..e47a94f56 --- /dev/null +++ b/client/ui/frontend/.prettierrc @@ -0,0 +1,12 @@ +{ + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100, + "arrowParens": "always", + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindFunctions": ["cn", "clsx", "cva", "tw"] +} diff --git a/client/ui/frontend/WAILS-API.md b/client/ui/frontend/WAILS-API.md new file mode 100644 index 000000000..494812d35 --- /dev/null +++ b/client/ui/frontend/WAILS-API.md @@ -0,0 +1,296 @@ +# Wails Go API reference (frontend) + +Reference for every binding method and model shape exposed to the frontend. Generated from `client/ui/services/*.go` via `wails3 generate bindings -clean=true -ts` — regenerate after any Go-side change. Authoritative source is always `bindings/github.com/netbirdio/netbird/client/ui/services/*.ts`. + +Every method returns `$CancellablePromise` (a Wails3 wrapper around `Promise`). Call `.cancel()` to abort the underlying gRPC call; in practice we just `await` and let it run. + +## Imports + +```ts +// Services +import { + Connection, Peers, ProfileSwitcher, Profiles, + Settings, Networks, Forwarding, Debug, Update, WindowManager, + I18n, Preferences, +} from "@bindings/services"; + +// Models (types-only) +import type { + Status, PeerStatus, PeerLink, LocalPeer, SystemEvent, + Profile, ProfileRef, ActiveProfile, + Config, ConfigParams, SetConfigParams, Features, + Network, SelectNetworksParams, + ForwardingRule, PortInfo, PortRange, + LoginParams, LoginResult, LogoutParams, WaitSSOParams, UpParams, + DebugBundleParams, DebugBundleResult, LogLevel, + UpdateResult, UpdateAvailable, UpdateProgress, +} from "@bindings/services/models.js"; + +// i18n / preferences models live in sibling packages, not services/models +import { LanguageCode, type Language } from "@bindings/i18n/models.js"; +import type { UIPreferences } from "@bindings/preferences/models.js"; +``` + +## Push events + +Subscribe with `Events.On(name, handler)` from `@wailsio/runtime`. Handlers receive `{ data: }`. + +| Event | Payload | Fires on | +|---|---|---| +| `netbird:status` | `Status` | Daemon SubscribeStatus snapshot — connection-state change, peer-list change, address change, mgmt/signal flip. Synthetic `StatusDaemonUnavailable` is emitted when the gRPC socket is unreachable, and a synthetic `Connecting` is emitted at the start of an active profile switch. | +| `netbird:event` | `SystemEvent` | One push per daemon SubscribeEvents item (DNS / network / authentication / connectivity / system). Used by the tray for OS toasts; the TS side reads events through `Status.events` instead. | +| `netbird:update:available` | `UpdateAvailable` | Daemon detected a new version (fan-out of the `new_version_available` metadata key). | +| `netbird:preferences:changed` | `{ language: string }` | Fires after every successful `Preferences.SetLanguage` (including the caller's own window). `src/lib/i18n.ts` subscribes and calls `i18next.changeLanguage`. | +| `netbird:update:progress` | `UpdateProgress` | Daemon enforced-update install progress (`action: "show"` etc.). | +| `browser-login:cancel` | (none) | Either the user closed the `BrowserLogin` window (Go-emitted) or the page's Cancel button (frontend-emitted). | +| `trigger-login` | (none) | Reserved by the tray for asking the frontend to start an SSO flow. `layouts/ConnectionStatusSwitch.tsx` subscribes and runs `startLogin()`; no Go-side emitter today. | + +The two stream loops behind `netbird:status` and `netbird:event` start automatically — `main.go` calls `peers.Watch(context.Background())` at boot. `Peers.Watch` is still exported but the frontend doesn't need to invoke it. + +## `Connection` + +```ts +Connection.Login(p: LoginParams): Promise +Connection.WaitSSOLogin(p: WaitSSOParams): Promise // returns email +Connection.Up(p: UpParams): Promise // async on the daemon +Connection.Down(): Promise +Connection.Logout(p: LogoutParams): Promise +Connection.OpenURL(url: string): Promise // honors $BROWSER +``` + +`Login` Down-resets the daemon first to dislodge a stale `WaitSSOLogin` (so a previously abandoned SSO flow doesn't fail the next attempt). `Up` always uses async mode — status flows back through `netbird:status`. **Do not call `Up` on an `Idle` / `NeedsLogin` daemon** — the daemon's internal 50s `waitForUp` will block and return `DeadlineExceeded`. + +Full SSO sequence: `Login` → if `result.needsSsoLogin`, open `result.verificationUriComplete` via `OpenURL` + `WindowManager.OpenBrowserLogin(uri)` → `WaitSSOLogin({ userCode })` → `Up({})`. The canonical implementation is `startLogin()` in `layouts/ConnectionStatusSwitch.tsx`. + +## `Peers` + +```ts +Peers.Get(): Promise // one-shot snapshot +Peers.Watch(): Promise // already invoked from main.go +Peers.BeginProfileSwitch(): Promise +Peers.CancelProfileSwitch(): Promise +``` + +`BeginProfileSwitch` and `CancelProfileSwitch` are normally driven by `ProfileSwitcher` / the tray, not the frontend. + +## `ProfileSwitcher` + +```ts +ProfileSwitcher.SwitchActive(p: ProfileRef): Promise +``` + +The single entry point both tray and frontend should use for profile flips. Applies the reconnect policy below, mirrors the switch into the user-side `profilemanager` (so the CLI's `netbird up` reads a consistent active profile), and drives the optimistic-Connecting paint via `Peers.BeginProfileSwitch`. + +Reconnect policy (driven by `prevStatus` captured at entry): + +| Previous status | Action | Optimistic UI | Suppressed events until new flow | +|---|---|---|---| +| Connected | Switch + Down + Up | Connecting (synthetic) | Connected, Idle | +| Connecting | Switch + Down + Up | Connecting (unchanged) | Connected, Idle | +| NeedsLogin / LoginFailed / SessionExpired | Switch + Down | (no change) | — | +| Idle | Switch only | (no change) | — | + +## `Profiles` + +```ts +Profiles.Username(): Promise // current OS username +Profiles.List(username: string): Promise +Profiles.GetActive(): Promise +Profiles.Switch(p: ProfileRef): Promise // raw daemon RPC; prefer ProfileSwitcher.SwitchActive +Profiles.Add(p: ProfileRef): Promise +Profiles.Remove(p: ProfileRef): Promise +``` + +`Profile.email` is populated by the **UI process** reading the per-profile state file (`~/Library/Application Support/netbird/.state.json` on macOS), not by the daemon — the daemon runs as root and can't read user-owned files. + +## `Settings` + +```ts +Settings.GetConfig(p: ConfigParams): Promise +Settings.SetConfig(p: SetConfigParams): Promise // partial update +Settings.GetFeatures(): Promise // operator-disabled UI sections +``` + +`SetConfig` is a partial update: only fields you set are pushed to the daemon. `profileName` + `username` are always required; the typed fields in `SetConfigParams` are optional (`field?: T | null`). `managementUrl` and `adminUrl` are always-string for historical reasons. + +**PSK mask quirk:** `GetConfig` returns existing pre-shared keys as `"**********"`. If you send the mask back, `wgtypes.ParseKey` fails on the next connect. `SettingsContext.save` drops the field when it equals `"**********"`. See `modules/settings/SettingsContext.tsx`. + +`SetConfigParams` carries one field that `Config` does not: `disableFirewall`. There's no current GET path for it. + +## `Networks` + +```ts +Networks.List(): Promise +Networks.Select(p: SelectNetworksParams): Promise +Networks.Deselect(p: SelectNetworksParams): Promise +``` + +`SelectNetworksParams.append=true` merges into the existing selection; `false` replaces. `all=true` ignores `networkIds` and targets every network (Select-All / Deselect-All). + +Exit-node filter: `range === "0.0.0.0/0" || range === "::/0"`. Domain network: `domains.length > 0`. CIDR overlap check is client-side. + +## `Forwarding` + +```ts +Forwarding.List(): Promise +``` + +`PortInfo` is a daemon-side oneof — exactly one of `port?: number` or `range?: PortRange` is populated. `protocol` is the lowercase daemon string (`"tcp"` / `"udp"`). + +## `Debug` + +```ts +Debug.GetLogLevel(): Promise +Debug.SetLogLevel(lvl: LogLevel): Promise +Debug.Bundle(p: DebugBundleParams): Promise +Debug.RevealFile(path: string): Promise // OS file-manager focus +``` + +**Log level case sensitivity bug:** `proto.LogLevel_value` is keyed on uppercase enum names (`"TRACE"`, `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`, `"PANIC"`, `"FATAL"`, `"UNKNOWN"`). `Debug.SetLogLevel` calls `proto.LogLevel_value[lvl.Level]` and falls back to `INFO` on miss. `useDebugBundle` currently passes `"trace"` (lowercase), which silently maps to `INFO` — the trace-capture flow doesn't actually raise the log level today. To raise to trace, pass `{ level: "TRACE" }`. Fix on the cleanup list. + +`Debug.Bundle` uploads when `uploadUrl != ""`. Result fields: `path` (local copy), `uploadedKey` (set on success), `uploadFailureReason` (set on upload failure — the local copy is still saved). + +## `Update` + +```ts +Update.Trigger(): Promise // start the install +Update.GetInstallerResult(): Promise // poll the outcome (long-running) +Update.Quit(): Promise // 100ms later, app.Quit() +``` + +Typical enforced-update flow on the `/update` route: call `Trigger` once, then poll `GetInstallerResult` every 2s with a 15-minute total timeout. On `success: true` call `Quit`. On `success: false` show `errorMsg`. If the gRPC poll itself starts failing for `DAEMON_DOWN_GRACE_MS` (5s), treat that as success and quit too — the installer commonly takes the daemon offline mid-upgrade. See `pages/Update.tsx` for the canonical implementation. + +## `WindowManager` + +```ts +WindowManager.OpenSettings(): Promise +WindowManager.OpenBrowserLogin(uri: string): Promise // uri appended as ?uri=… +WindowManager.CloseBrowserLogin(): Promise +WindowManager.OpenError(title: string, message: string): Promise // custom branded error window; both query-escaped as ?title=…&message=… +WindowManager.CloseError(): Promise +``` + +Prefer `errorDialog({Title, Message})` from `lib/dialogs.ts` over calling `OpenError` directly — it's the app's single error surface (the old native MessageBox wrapper now routes here). Both strings must be pre-localised. + +Both auxiliary windows are created on first open and destroyed on close (mutex-guarded singleton). The BrowserLogin window's red-X close fires the `browser-login:cancel` event so `startLogin()` can tear down the pending daemon `WaitSSOLogin`. + +## `I18n` + +```ts +I18n.Languages(): Promise // from _index.json +I18n.Bundle(code: LanguageCode): Promise> // full key→text map +``` + +Source of truth is `client/ui/i18n/locales/` (shared with the Go tray). The frontend's i18next bootstrap doesn't need `I18n.Bundle` at runtime (bundles are statically imported by Vite via the glob in `src/lib/i18n.ts`), but the language picker reads `I18n.Languages()` so the list matches `_index.json` without duplicating it in TS. + +## `Preferences` + +```ts +Preferences.Get(): Promise // { language: string } +Preferences.SetLanguage(code: LanguageCode): Promise // rejects on unknown code +``` + +`SetLanguage` validates against the loaded `i18n.Bundle`, persists to `os.UserConfigDir()/netbird/ui-preferences.json`, and emits `netbird:preferences:changed`. The frontend's `src/lib/i18n.ts` listens to that event and calls `i18next.changeLanguage` so a flip in any window paints in all of them. Missing preferences file → defaults to `en`, written on first read. + +## Daemon `Status.status` values + +Mirror `internal.Status*` in `client/internal/state.go` plus the synthetic UI label: + +| Value | Meaning | +|---|---| +| `"Idle"` | Tunnel down (Up never invoked or Down completed) | +| `"Connecting"` | Up in progress | +| `"Connected"` | Tunnel up | +| `"NeedsLogin"` | Fresh install or token cleared; needs Login → SSO → Up | +| `"LoginFailed"` | Previous Login attempt errored | +| `"SessionExpired"` | SSO token expired; needs re-Login | +| `"DaemonUnavailable"` | **Synthetic** — UI side, emitted when the daemon gRPC socket is unreachable. Not a real daemon enum. | + +The tray also reads a tray-only synthetic `"Error"` for icon purposes; the frontend doesn't see that. + +## Model field reference + +`Status`: +```ts +{ status, daemonVersion: string; + management: PeerLink; signal: PeerLink; + local: LocalPeer; + peers: PeerStatus[]; + events: SystemEvent[]; } +``` + +`PeerLink`: `{ url: string; connected: boolean; error?: string }`. + +`LocalPeer`: `{ ip, pubKey, fqdn: string; networks: string[] }`. + +`PeerStatus`: +```ts +{ ip, pubKey, fqdn, connStatus: string; + connStatusUpdateUnix: number; + relayed: boolean; + localIceCandidateType, remoteIceCandidateType: string; // pion: "host"|"srflx"|"prflx"|"relay"|"" + localIceCandidateEndpoint, remoteIceCandidateEndpoint: string; + bytesRx, bytesTx, latencyMs, lastHandshakeUnix: number; + relayAddress: string; // set when relayed=true + rosenpassEnabled: boolean; + networks: string[]; } +``` + +`SystemEvent`: +```ts +{ id: string; + severity: string; // "info"|"warning"|"error"|"critical" (lowercased proto enum, "SystemEvent_" prefix stripped) + category: string; // "network"|"dns"|"authentication"|"connectivity"|"system" (same casing rules) + message: string; // technical / log line + userMessage: string; // human-friendly — render this + timestamp: number; // unix seconds + metadata: Record; } // keys: "new_version_available", "enforced", "id", "network", "version", "progress_window", … +``` + +`Profile`: `{ name: string; isActive: boolean; email: string }`. + +`Config` (read-only mirror, all required): +```ts +{ managementUrl, adminUrl, configFile, logFile, preSharedKey, interfaceName: string; + wireguardPort, mtu, sshJwtCacheTtl: number; + disableAutoConnect, serverSshAllowed, + rosenpassEnabled, rosenpassPermissive, + disableNotifications, lazyConnectionEnabled, blockInbound, + networkMonitor, disableClientRoutes, disableServerRoutes, + disableDns, disableIpv6, blockLanAccess, + enableSshRoot, enableSshSftp, + enableSshLocalPortForwarding, enableSshRemotePortForwarding, + disableSshAuth: boolean; } +``` + +`SetConfigParams` has all `Config` fields as `field?: T | null` (partial update), plus the write-only `disableFirewall?: boolean | null`, plus `profileName` / `username` / `managementUrl` / `adminUrl` as required strings. + +`Features`: `{ disableProfiles, disableUpdateSettings, disableNetworks: boolean }`. + +`Network`: `{ id, range: string; selected: boolean; domains: string[]; resolvedIps: Record }`. + +`ForwardingRule`: `{ protocol: string; destinationPort: PortInfo; translatedAddress, translatedHostname: string; translatedPort: PortInfo }`. + +`PortInfo`: `{ port?: number | null; range?: PortRange | null }` (exactly one populated). + +`PortRange`: `{ start, end: number }` (inclusive). + +`LoginParams`: `{ profileName, username, managementUrl, setupKey, preSharedKey, hostname, hint: string }`. + +`LoginResult`: `{ needsSsoLogin: boolean; userCode, verificationUri, verificationUriComplete: string }`. + +`WaitSSOParams`: `{ userCode, hostname: string }`. Resolves to the user's email. + +`UpParams` / `LogoutParams` / `ProfileRef` / `ConfigParams` / `ActiveProfile`: all `{ profileName, username: string }` (different names but same shape — kept distinct by Wails for clarity). + +`DebugBundleParams`: `{ anonymize, systemInfo: boolean; uploadUrl: string; logFileCount: number }`. + +`DebugBundleResult`: `{ path, uploadedKey, uploadFailureReason: string }`. + +`LogLevel`: `{ level: string }` — **uppercase** proto enum name (`"TRACE"`, `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`, `"PANIC"`, `"FATAL"`). + +`UpdateResult`: `{ success: boolean; errorMsg: string }`. + +`UpdateAvailable`: `{ version: string; enforced: boolean }`. + +`UpdateProgress`: `{ action: string; version: string }`. diff --git a/client/ui/frontend/eslint.config.js b/client/ui/frontend/eslint.config.js new file mode 100644 index 000000000..f00623b68 --- /dev/null +++ b/client/ui/frontend/eslint.config.js @@ -0,0 +1,75 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import jsxA11y from "eslint-plugin-jsx-a11y"; +import globals from "globals"; + +export default tseslint.config( + { + ignores: ["dist/**", "node_modules/**", "bindings/**", "sonar/**"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.{ts,tsx}"], + plugins: { + react, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + "jsx-a11y": jsxA11y, + }, + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { ...globals.browser }, + parserOptions: { + ecmaFeatures: { jsx: true }, + }, + }, + settings: { + react: { version: "detect" }, + }, + rules: { + // ----- a11y / semantic HTML (jsx-a11y recommended) ----- + ...jsxA11y.configs.recommended.rules, + "jsx-a11y/no-autofocus": ["warn", { ignoreNonDOM: true }], + + // ----- React ----- + ...react.configs.recommended.rules, + ...react.configs["jsx-runtime"].rules, + "react/prop-types": "off", + "react/jsx-no-target-blank": ["error", { allowReferrer: true }], + "react/self-closing-comp": "warn", + + // ----- React hooks ----- + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + + // ----- Vite / HMR (Fast Refresh) ----- + "react-refresh/only-export-components": "off", + + // ----- TypeScript ----- + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/consistent-type-imports": [ + "warn", + { prefer: "type-imports", fixStyle: "inline-type-imports" }, + ], + "@typescript-eslint/no-explicit-any": "warn", + + // ----- General correctness ----- + eqeqeq: ["error", "smart"], + "no-console": ["warn", { allow: ["warn", "error", "info"] }], + "no-debugger": "error", + "prefer-const": "warn", + }, + }, +); diff --git a/client/ui/frontend/index.html b/client/ui/frontend/index.html new file mode 100644 index 000000000..e62139956 --- /dev/null +++ b/client/ui/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + NetBird + + + +

+ + + diff --git a/client/ui/frontend/package.json b/client/ui/frontend/package.json new file mode 100644 index 000000000..3131b36cd --- /dev/null +++ b/client/ui/frontend/package.json @@ -0,0 +1,69 @@ +{ + "name": "netbird-ui", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build:dev": "tsc && vite build --minify false --mode development", + "build": "tsc && vite build --mode production", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "bindings": "cd .. && wails3 generate bindings -clean=true -ts", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"", + "lint": "eslint \"src/**/*.{ts,tsx}\"", + "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix", + "check": "pnpm lint && pnpm typecheck && pnpm format:check", + "check:fix": "pnpm lint:fix && pnpm format && pnpm typecheck" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-visually-hidden": "^1.2.4", + "@wailsio/runtime": "latest", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "framer-motion": "^12.38.0", + "i18next": "^26.2.0", + "lucide-react": "^0.566.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^17.0.8", + "react-loading-skeleton": "^3.5.0", + "react-router-dom": "^7.1.3", + "react-virtuoso": "^4.12.5", + "tailwind-merge": "^2.6.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^25.6.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.39.4", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.6.0", + "postcss": "^8.5.1", + "prettier": "^3.8.3", + "prettier-plugin-tailwindcss": "^0.8.0", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", + "typescript": "^5.7.3", + "typescript-eslint": "^8.61.1", + "vite": "^6.0.7" + }, + "packageManager": "pnpm@11.4.0+sha512.f0febc7e37552ab485494a914241b338e0b3580b93d54ce31f00933015880863129038a1b4ae4e414a0ee63ac35bf21197e990172c4a68256450b5636310968f" +} diff --git a/client/ui/frontend/pnpm-lock.yaml b/client/ui/frontend/pnpm-lock.yaml new file mode 100644 index 000000000..b6b3dd336 --- /dev/null +++ b/client/ui/frontend/pnpm-lock.yaml @@ -0,0 +1,5240 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.3.8 + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wailsio/runtime': + specifier: latest + version: 3.0.0-alpha.79 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + framer-motion: + specifier: ^12.38.0 + version: 12.40.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + i18next: + specifier: ^26.2.0 + version: 26.3.0(typescript@5.9.3) + lucide-react: + specifier: ^0.566.0 + version: 0.566.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-i18next: + specifier: ^17.0.8 + version: 17.0.8(i18next@26.3.0(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + react-loading-skeleton: + specifier: ^3.5.0 + version: 3.5.0(react@18.3.1) + react-router-dom: + specifier: ^7.1.3 + version: 7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-virtuoso: + specifier: ^4.12.5 + version: 4.18.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.1 + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@9.39.4(jiti@1.21.7)) + '@types/node': + specifier: ^25.6.0 + version: 25.9.1 + '@types/react': + specifier: ^18.3.18 + version: 18.3.29 + '@types/react-dom': + specifier: ^18.3.5 + version: 18.3.7(@types/react@18.3.29) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7)) + autoprefixer: + specifier: ^10.4.20 + version: 10.5.0(postcss@8.5.15) + eslint: + specifier: ^9.39.4 + version: 9.39.4(jiti@1.21.7) + eslint-plugin-jsx-a11y: + specifier: ^6.10.2 + version: 6.10.2(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-react-refresh: + specifier: ^0.5.3 + version: 0.5.3(eslint@9.39.4(jiti@1.21.7)) + globals: + specifier: ^17.6.0 + version: 17.6.0 + postcss: + specifier: ^8.5.1 + version: 8.5.15 + prettier: + specifier: ^3.8.3 + version: 3.8.3 + prettier-plugin-tailwindcss: + specifier: ^0.8.0 + version: 0.8.0(prettier@3.8.3) + tailwindcss: + specifier: ^3.4.17 + version: 3.4.19 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.19) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.61.1 + version: 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + vite: + specifier: ^6.0.7 + version: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-visually-hidden@1.2.4': + resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.29': + resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==} + + '@typescript-eslint/eslint-plugin@8.61.1': + resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.61.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.61.1': + resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.61.1': + resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.61.1': + resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.61.1': + resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.61.1': + resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.61.1': + resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.61.1': + resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.61.1': + resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.61.1': + resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@wailsio/runtime@3.0.0-alpha.79': + resolution: {integrity: sha512-NITzxKmJsMEruc39L166lbPJVECxzcbdqpHVqOOF7Cu/7Zqk/e3B/gNpkUjhNyo5rVb3V1wpS8oEgLUmpu1cwA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.362: + resolution: {integrity: sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.3: + resolution: {integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.1: + resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.3: + resolution: {integrity: sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + framer-motion@12.40.0: + resolution: {integrity: sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + i18next@26.3.0: + resolution: {integrity: sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA==} + peerDependencies: + typescript: ^5 || ^6 + peerDependenciesMeta: + typescript: + optional: true + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.566.0: + resolution: {integrity: sha512-b18qC/JAh1X9rVKlF5EtSIyumdIYuh78b0JShynZnHbcaWR4AW4oZyi8Ms/aQYVSnLPlAnMhug2hSr19BgVZAw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + motion-dom@12.40.0: + resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-tailwindcss@0.8.0: + resolution: {integrity: sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g==} + engines: {node: '>=20.19'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-i18next@17.0.8: + resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} + peerDependencies: + i18next: '>= 26.2.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 || ^6 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-loading-skeleton@3.5.0: + resolution: {integrity: sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ==} + peerDependencies: + react: '>=16.8.0' + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-dom@7.15.1: + resolution: {integrity: sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.15.1: + resolution: {integrity: sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-virtuoso@4.18.7: + resolution: {integrity: sha512-xNF5zDGEEIMB7cKwcen/pLig0YDf6OnfFrVgKFa7sHPf9fRem0CaLshyObbBcP88jzn0enavL39EgplgdyT21g==} + peerDependencies: + react: '>=16 || >=17 || >= 18 || >= 19' + react-dom: '>=16 || >=17 || >= 18 || >=19' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@2.6.1: + resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.61.1: + resolution: {integrity: sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))': + dependencies: + eslint: 9.39.4(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@10.0.1(eslint@9.39.4(jiti@1.21.7))': + optionalDependencies: + eslint: 9.39.4(jiti@1.21.7) + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.11': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-context@1.1.2(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-direction@1.1.1(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-slot@1.2.4(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.29)': + dependencies: + '@types/react': 18.3.29 + + '@types/react@18.3.29': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 + eslint: 9.39.4(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3 + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.61.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + + '@typescript-eslint/tsconfig-utils@8.61.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@1.21.7) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.61.1': {} + + '@typescript-eslint/typescript-estree@8.61.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.61.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@4.7.0(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) + transitivePeerDependencies: + - supports-color + + '@wailsio/runtime@3.0.0-alpha.79': {} + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + autoprefixer@10.5.0(postcss@8.5.15): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.12.1: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.32: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.362 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001793: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.362: {} + + emoji-regex@9.2.2: {} + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.1 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.3: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.3 + + es-to-primitive@1.3.1: + dependencies: + es-abstract-get: 1.0.0 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@1.21.7)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.12.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.4(jiti@1.21.7) + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@1.21.7)): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 9.39.4(jiti@1.21.7) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.3(eslint@9.39.4(jiti@1.21.7)): + dependencies: + eslint: 9.39.4(jiti@1.21.7) + + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.3 + eslint: 9.39.4(jiti@1.21.7) + estraverse: 5.3.0 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fraction.js@5.3.4: {} + + framer-motion@12.40.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.40.0 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@17.6.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + i18next@26.3.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.3 + side-channel: 1.1.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.566.0(react@18.3.1): + dependencies: + react: 18.3.1 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + motion-dom@12.40.0: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.46: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + possible-typed-array-names@1.1.0: {} + + postcss-import@15.1.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.5.15): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.15 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.15): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.15 + + postcss-nested@6.2.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-tailwindcss@0.8.0(prettier@3.8.3): + dependencies: + prettier: 3.8.3 + + prettier@3.8.3: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-i18next@17.0.8(i18next@26.3.0(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.7 + html-parse-stringify: 3.0.1 + i18next: 26.3.0(typescript@5.9.3) + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + typescript: 5.9.3 + + react-is@16.13.1: {} + + react-loading-skeleton@3.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.29)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.29 + + react-remove-scroll@2.7.2(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.29)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.29)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.29)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.29)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + + react-router-dom@7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-router@7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.1.1 + react: 18.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-style-singleton@2.2.3(@types/react@18.3.29)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.29 + + react-virtuoso@4.18.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.8.4: {} + + set-cookie-parser@2.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-json-comments@3.1.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@2.6.1: {} + + tailwindcss-animate@1.0.7(tailwindcss@3.4.19): + dependencies: + tailwindcss: 3.4.19 + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-import: 15.1.0(postcss@8.5.15) + postcss-js: 4.1.0(postcss@8.5.15) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.15) + postcss-nested: 6.2.0(postcss@8.5.15) + postcss-selector-parser: 6.1.2 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@7.24.6: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.29 + + use-sidecar@1.1.3(@types/react@18.3.29)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.29 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.9.1 + fsevents: 2.3.3 + jiti: 1.21.7 + + void-elements@3.1.0: {} + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/client/ui/frontend/pnpm-workspace.yaml b/client/ui/frontend/pnpm-workspace.yaml new file mode 100644 index 000000000..5ed0b5af0 --- /dev/null +++ b/client/ui/frontend/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/client/ui/frontend/postcss.config.js b/client/ui/frontend/postcss.config.js new file mode 100644 index 000000000..2aa7205d4 --- /dev/null +++ b/client/ui/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/client/ui/frontend/src/app.tsx b/client/ui/frontend/src/app.tsx new file mode 100644 index 000000000..c7b12e538 --- /dev/null +++ b/client/ui/frontend/src/app.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./globals.css"; +import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; +import SessionExpirationDialog from "@/modules/session/SessionExpirationDialog.tsx"; +import UpdateInProgressDialog from "@/modules/auto-update/UpdateInProgressDialog.tsx"; +import WelcomeDialog from "@/modules/welcome/WelcomeDialog.tsx"; +import ErrorDialog from "@/modules/error/ErrorDialog.tsx"; +import { AppLayout } from "@/layouts/AppLayout.tsx"; +import { MainPage } from "@/modules/main/MainPage.tsx"; +import { SettingsPage } from "@/modules/settings/SettingsPage.tsx"; +import { SkeletonTheme } from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; +import { welcome } from "@/lib/welcome"; +import LoginWaitingForBrowserDialog from "@/modules/login/LoginWaitingForBrowserDialog.tsx"; +import { initI18n } from "@/lib/i18n"; +import { initPlatform } from "@/lib/platform"; +import { initLogForwarding } from "@/lib/logs"; + +// Must run first so even init-time logs reach the Go log pipeline. +initLogForwarding(); + +welcome(); + +Promise.all([ + initI18n().catch((e) => { + console.error("i18n init failed:", e); + }), + initPlatform().catch((e) => { + console.error("platform init failed:", e); + }), +]).finally(() => { + ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + } + /> + } /> + } + /> + } /> + } /> + + }> + } /> + } /> + } /> + + + + + , + ); +}); diff --git a/client/ui/frontend/src/assets/fonts/inter-variable.ttf b/client/ui/frontend/src/assets/fonts/inter-variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4ab79e0102bbe0ffa1ed879b13e52ac8c6487833 GIT binary patch literal 879708 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 z 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

R*jRJ1?^iQ`xYAuqn>-7iqg^r+ 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

!+-?iHy`H`{?S!=5pudjt1=3|0 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⁣dUi zriw@-rkhk&MwpzSL`8)QVHq?RggwfNi_1!iNIHwl@O^;`9u|eR!qF(02f~R70Wj}8 zbna3|hh0rdQF4yLoL@}Li;O};h>ni2goRncL&2}mFblpnnWLkkXkcgPOa!-R_WVoW z*5lxoZ}$A;f`aqsVWkj<3Pc;q%*xI_E4~Ag^vc9nIXTA;pUy|2YnOo)r);xA-bYIxGZSyguqYK`0n(|wIJ!x8(z2jJ2{kPk`|4F4#*xL8Y zQ`#>Ic5XXUkIw%AJI;kpL3i^f-G})|64JccuGxIxU$4CKN*ca>xc3)xW6v+t<=&y( z=|mVO1)TcEm@qOa$zq5wl3}05*79qG>G8*|8$T*0ajYdEq@w|o+|v+dqbptehd(SB z85zT1_1oFPSEbfcyIeXI)4OeSNjF8>CCrkOTKa$e>oMt*3kxeNVG31f$avT>KnkoQgt!23tQdy0uHKucXfEfww21(!8s%Y($LzE_115gG zbNi0M=(gC!kIffEXIrHa(r+b=)~W4ki;YT}o9s-H0&v}PPa4a_^&KK%&mu1R@2uoy4G^P@Sg`ru)NQ9XfjR|>| z%0;wdTJt9ygXQJqV95aS7RyuYhHj0720O^ z@?>SA8Yd-gf8z$6vGy<=*q1QR`ayyCzNf z0x{TBCD`+>vV?u*kX6IdmPZ!nONxt$#B486Rk1D*ucfZN+X*jp zd1RQ;7!YU<4>Jb^nZhC>!o$Ldj#&D8QBp4BH-R#rTh(l>RmGmO%Iqc>Y>9k81#&=< zA7qLPN*3J?-RR|gBdZb%par4u3JcGlFDQVw>w>zbh*utri+pOxc$;_reD)56D?Rpb zGEvBw=wO)11u10;oCt@J{9y$vW;C!1S=jeK9^3fF8*hBE>-)W1k3lmW{OZT==OHF= zHNXB8`s4TNnKNgG?EIlR5<_6_+S-#Jq*72pc`jh}k6U+se;yIE({C-HJ`Wep%ENV+ zv;w?&#;!Yj_{i~du3EPfy0$3u#F4pk=NgK>OM^YguYk=i-Q2BPx0ObZ#Y{Ly^z(Y= zF^^52I|;A3bH@fWwXi3ix@K%t$u=hoTR8vj2k*M;u6utqf8jJlGZU@j=P#Ux=u!c{ zR*qaaUxB}_Tt90>{#RdpRTdq6^_}Xa!_PGjWcNmH(z5AD)E=|Qhj4SeZ=DP7t3-h1yYW1DuQ!@ahd^-5!t(Lk$@ z3;|6|2>aq&&C9!+M&Ep|GjZ(DtVEDVFjO_RkU|uNaPulF3k%_kyI_J)jJFu3QxViL z-eC;9Y%g@0Y!BzrC8%N@^Jp|mrEA*iSdmr_(2 z2j$73g}Ft*xiB|33(Yz|J1^6#x7rxG$%JRslA=ZVsAu)x7)ujMx41=M@H_$K}kLuR6$8eIof|wS#gEy zK&YCmo-Q&ha85eHuh4V-2m-PQf|$aC5luG1-GC7kY6*)nkLd^8Er7XRhyayo*5+_% z%m`390vgi@T4rSCo_7_}+9(^-Ihby_X4dJ}mu=gVmBs1+-CNkN9oF7qM>0FQTs}E( zUK-niekPUuOI3F8O2~IQepVjD6zvC>8&Qj@ijqoKK?y868|!eej^##R7ZVj8Z8nAT z%`5qrg6|q&|M}2&DTw>Pg zbJS&LqKRSP7_aPer_Y@`bLJe3aKu5-C6JYugAQGzK|5hedLTqh@!;f755=eKfhw#m zg5e3j8$##22EFwZT+@^p{JK)P$MD+P?TC^eIdWp#b#P0@Vx)b9lBkTsJGv5;>B`;u z*Wcd`G=BUmnl8M@Th14_N@}~+j@XvAVZZ#q56ITej?9C7s=) z7CWzFb5-?2m}= z+2kjlz<}s#mV*v{sa^X~OL3`Yc#t!+^i1{e#Ov<6bz1DZpBya8*;+d4k0PA@rsk=+ z5$E1s;Y=*QP)!|q1-f+%Xya8@eW9986o%1`NCtW+GVe;!&1*WzRF~2N6^|le0iRZ; zC@97fVamdJL8M6Oayt_vhs9E#9)%trgWz~sVuxWVPkby4cmyuckCAy7fgbnR8iZ&{^eVlZki!Tt*HAV-qF_ zXwa@YVZR%3k}75f!Rdg;V~NdIFDvbE{^N(Fl+dI__uY5jqNLE4(|h;s%khLx{q=3P zO$~LQK7G1G#!S9skIT+<$(~v)UXhep%0wH63ojhmgpteVgqg3guiJE_R>d`qVcn*k z7!BUJ>C4y9XJKjF?65Spm#tw7+2P<9{*V+uA@D4MBWH{g(wSwWs;EI>txRV*=K?m@6~&~zfbRncQ%}v%*;76v-jF- zul1~FtxetmN)^LNqU_15MMWCcbK0tCEKS#lUTAm8Ue&7UUS#73>)u(4sV%}sVc95nm3eD3+{1Mr2!*+9|SE4)TIO#xKbojC=?Ruz}c>ojuDNMY^iN}7Un2J#FbTnBWi<3+yf#)tmhXjfX zFYcNMTl)pRKc<{{$K2sCtyl9OTJ64`_wx^*Z~qaW)L8dJDF2-D_HEntI}dUauzX`q zE}}Y3%S=&gdfD|I@*u0Sqf2fF)W(Y0x&v{Mdktk>KH=fvsV}^ZP*)g0?(Jl)PKG5v zL*eGOVggCWDr1E0eTsZ1G;C7qw04nS;EM~FPVxy6wQ%Ky4#e65 z%gtm0rwg6i1<+?8Ca0D6m}yY~-rgR-vE1EVU7XZF;a!~7=oEO^jzAdwM~)4k6B;G) zV-&nV*0E5~t9xM4g+}<-%iD`VQ84j!=J+Jl^%BSk#n+$cdJbGmT~E|Hl0I^zhk=~= zj2CdmbeypUXFLz7qFL^NDQ(@ki5VGseR_H#tirU1cYyRIrKH`ydH??X8#mLk(<#3q z6Bov&q9y4~O`Ks47zH`}k z;8h$gbsirH2{RZH?mMTlO4Li*@nB;61i-g~RB7=oH3_hUA9!`y=CP#0iJ9;^|`1^y$ z6A&eGnAt$GYjWvh{uh;(NC{X9nX zOK&mR2K`M(b)z>!O~b*p`UFvwvM&l~3C@MJ%b@BaD9wTo$aM9Zh)IhsF9x#;z;khtzN8ecAPn$A5b^-AmJ}Bip#SMJ#R)M0z6uZ>!dIh;#b=&V zHagfDsJz^1;K*kezPbEh0Skd$`V_dY74x_R~ECZ@v zla%yRIKX2x$==i!WzbS=yrcD{BWEZxU<3tQkGhJbWrq9$y@1MMH7P|IQToH+R53bO zbnpeO8Pbi-ywL@)`+(YMed!pe9n}~M!N8;bp@X9ud#MYsES3!i;`d*#Vjh!FjL9D8 zg~QMbp^%bLYy6BMF?2@6ADy`-utT9y16wu83#xC}S_om2ZYQ_LL( zOYS|S0O|18cs>X|z$l=1ukq`pV3M}pJoQa9tdHoPHiBOueZ_cc`XTL zoRlk9jt`%+eBSFDH*Wl32HA#~RRYNa;X&Hd#Ed%=$;@P;nP-{dOfX!5K*oukKmX+^ zNZi{iv8eaTwlX+;ex>tE1nq?;%3P~`D$!beM3S#6mpq` zQt9F2=|6^J9)8gyIm}l%oLn9lGIGqAF@Z|0rx_zEK$PELsHm-}1cYBvURYcTe796z zcq;}BSnzilK(d^W^n+0Y)u*$Q!l!V$JZ)%D-^Ln|ezn@!6B~n5aMuPY?oLh)DjWa* zcD`9+CPHGUeXt0#cp@ZbBJ2Y{NHA6axPDNIfIjdLW@Zkoe=HYxL~pq?3MS)09)T!# zb5a3+mWnh$a)Dey#5g$LI@_YsTIS608|A7=xCJWmgveVbD4IL_EadtM;J4j7B+X2cZCa8~xMHBrJO^!i9 zFHi&sR-f#RH)?C^+(*9_iNP~Bjp&^zWEJcUS;=e!!?w~U9>$WJAJLpNVK&Qd$Q6p) zx8-t;qzTM+wMyp)Wd&Uf?@b6|(1$PzA<}v%Zaktl0}YGG81RHBV8S6kkE*F|ji|dD zGn@kQ=m4FcJ#ZetIuz@%u@N4^0(68&05yz9^rj{Of8$Cqlv^ReNd3F*C*-StO)b7f z^iWL?-*IN!Z!XSQhOu3Tv7L*torAF*053i(m$LHmv+*zmK7BU)d2}Y|IDk;{b23x! z+{{nTN>6+A2zB^a_zkf>Fm$1{Sc$N(hzZ4_vAtmq;DC~NdMz_zvQ zl8FXvet__Y`?_(CZ1%2QyKYt4qO?Ou@AACMac6!jG?e`E3sRGmH*QuyAvYGMfC3eU z5wU0XG^3cI>;871z#v8O^>4oUCO+$}VGt^r?7j7v$17K9!vV%uk(;s)CduIQxW7#! zZWqW+ra!^nVPv=UqL*J=1UPZ}xFCQ!R#PXU;LbPXG>M0gcMivMB$ZwOo=Y;v&UWq4 z_|>;7z>M`DI^yBp3#l1-CEW_Iu&^*s(~o&{q^uG8-nQ*@v33BhCt)a|asW^>7r-|`+lR+r3m)iE@-;A@)gU~2IPRLX zY~{+8%gZU@#B9nt;he7mE+Ve;_<2{U3479+f&;lDU{_CLyMrGf)_5hL$PvFs`Tz&S#`*8S@ zvr9`5tj*Ks*IVuH*40JitiGfc z_IUzot+qu9cc*XP*S)Rw@yyAVwWmMEb;yXE`jKnateHOU#ZX5RW<^JfQ6wfym(B=(_Jyj4SuIe$ zQrV2TOSKkmE$J!H8M9{xQy6<~)yFpD7T4%CeBp=45mBaPRw(?3Pn|n=?xdhF zIG%^ZAI_Te>Z=o>Vs4NJhzwqVz4$%;>q$1b3N51NJjG(?zJ2V2qL{V}W(wk4&ncS* zcKwXY?m3&9ZfMsyfevjX;!i*Q{CYPTKhs$P9ckxq;@eNPqpFO26OadhN=;2wWmO&B zz|mUEOWUkTenw()CEwmkL*7zjhoPjoBe%Sxsk*j}NQ@o8O`5q8a(P`wMqNXFMWvCa z+-5szlclQyv1^qkTnb}kV&!#pthlngrcP}G7Gi62V{@H$lta+a-Z(`nwY0al zvao_TfE&oGmNu4hF=JtCB~f{Md%HXKu~YX87TJs&F?h(BsEOgheLb+RjHl69{xDe& z#(NGgS3-qbnTl1&i87hwc6QbhpiZ3#eVB}GZA?dgmZ4T{jpV_WrsAr$KUPe5!V{E0 zCTZR=iAQ+|ngvJ}(?g=UghE}Kdx(PtgQzAQ`gemhA57t zHUC}-$T6UgJEREHM^-&29*PEX#?S$<}fjjl9J-% z6FWrk=NjwkTa1j>DqG)f>+9?5Y1NT>^!o(FrR*+{%VAGUw^rd0vZ84-`!G;6b&=$e_SMBg25f9TmGc`h+hDr-kXr z4%M2M=Js#I!rmGX&M-+yrKQmV*PlW!M?hN9N&(TVd7aWW4{^OwprEu-Z0i9@89V1i z@pZWvwtz`JWZs)^zWJ<+P3^h8d-qyVm<1Ler;LG%oBJ>^;X-`oV{ORKalc+d6zIyi zbLY@UBb`svW2L%)_dBqZFvee2epJ4s15&;R_>Pe1*9`ZTuU z!*9p(g&-AfVyPJT10}$-G7+uBD1|r@AaZ;YXfMi6fMjwT5R{Z+Uh7|9-%1mIy4%{Z zJhmgPq^Y?b>>guVLt|Y9EZu6ct6D9QaMsjg9y-LPs;a6MX?qO)MBL1uC=!d|2MCmV z#EOkMz#491^xRRWR6QsK~I;~l_AFlI4%sNE)8AWkoT1knSkxnbZEtOe81`?HhXGdbz znRAZ*A%1O#JIyiMlN8LEo)9e>8Y)B})ia|_qaa-?jZSU@;b}A7Vn(l6y%-h?_ zN?~lL>n+<>W2mTkT+%9oeGFu#tG2ogo^J5iBa3u zqbXKk)K+5DhJt30S5#D~hx=0i>TU%qAwn0yI)O2X0z7q)~XP#ip+Ms`>_6g|@b~g8&yXQ8Cj=GUDQPR8@ge#Anbz zy+m`aK%#c0@%+{NTdEx=uH-_^m)!sL$h|iBAm>B*mzYnsMT54q8hAJD_*eP0qMcv< zLq`L*Gh>ht{|rl>8#1JL-MXS*kivG#(AHK_hyJR|N`H`;a6SG(X{)I>A_6Fxo7*jM z9U2}nW|*HyD{-AXd0AUqBp-^fC-PLtZt-g~20GY;J?+D=S+CzKPt< z#S?~cptpn6Sc8rQ6VAT9p&~muDJi*3q#X)AN@-zXh93SG7*cxN18ETE(an<=i(Bfc zHG?;lmQgRQvaYVWysVs3K~zR-YrD=)h=!Q*jd0VtA<9%-1wd7$h6jz8?d)U(Fb!4S zCf!gKyfzpOD)?styAgLPg*8B2m0Ry#_PyNkz#4CW;|&{K)NFF^>*qw7Br0QTD;z=o z$tM}RweDgIROUeZ0CZ##7G%8K(%ey1Qq|TCVuyJ()IioYL)LWA5U)bkUWKe#Ubvcy z9|UXV+NFKFX_6IfZ-J@1;}cjt+KHeCz6q|)I&|b>gjd&MnBU;v7HKq^m}r5Gv+_w< zS9Zq%%%Vvg$JRW&#rYQ$Q5yp>jtWDOzNjE81yk_KH?b#gL;a*aN>4}6W1XvwReYKt z0UP7nZ@)coAe3Jr-i#t|KzKGvwtj>7|Gp@Q;tE*&gAq6y$Q=H1D!$xu{@LH@#S;-k zdWQEE+A}BxN!gSU9LMDE$Hh4R?b|_R`Nz3(aldDt2^!?%s)PX8INJv=TD59bn3L(w zjcYeka*LZ{C93*VL>hc+;gwIwRrmI(W57?r-_jXSPy&{S9ac# zbyJWrlg&8NM#^Ed@r)hQM`-HcP+Y;E?bD4`u8?C=ZMUOoE0oR)szZaO`eCAhOfOb;zh8!$u4r?WThE zka`6S00C#9BZp0rIM&M5o%0u~5EWF|SX&|vF431fZa_j?bw$mC44htHTWzhhv_fdt zGB$XkCpF-p$GshV`-18+ed5Sam;sY!%v!ea#g}JIUh>K;B(}_bW&WHQGiC&PjX+8= z$I9&;Ie$wR%s&TDdnLr1X>Dn0hjZR7wRaaV36oJSl`Dv&ojlfz_h4&e(hIWY2U&A~ zticnsLh$~nn^h&UD;EYaNuW8K4`6aV?>c`{8Wo`#FCaF`Z|Y;C?y z3FGHK^=P%_uN13he-<0jh$Wb1`r>@^F`gK8#29O7rBb zw4;Esvj9k|ppb!&B3X&zIU*ofIvK``-T-^@TBYjn126RZi~(#{9Hk;IvF=2Isxj-r zDcs_Ij8oCyJs%(iINdU6pVUIT85;o+%ieqMy^yvxxk6*n=lL0S%E7NJTv*PLa;Z&wQ4B3`12Wk;>$ti0Foh`Gy`FJJM}gox2$W?=Xc>#ikpn!oO14)M77!C;(K6dsh%7(GDwnZj{lgb8gw7mm@2Na30qm(7$_g^m} z*gbX?E`PFWZyVVE#V7$oox5uVB{yl4$H zjqe#6Ixw7+4h-^Nh|TuxrKSJNz$kBz8g={c3=Ro{w|4=x<^GpJvQ*d9(BlR-qk=)fn1H~DaE^nfC|kZ9b88J}@1$t2YZCj7v9THCqi|*f&ZH5t8PG>h zvP*pOi;I6(Km`SeyJpioRQL{1QDD~;7egP(8ETax_Q%JnfVEOh-qlTKEmk>3r#gJ^ z-iaUMsOq_pWu(C*ni;yp;$oL*098_+$zhasizH`gBd>sfxVRJfGII4=C6);(0&h2w z!m$0bt0Z_#APET2sUr0H;SuvzK<~_p8a>dCm50$VyTz*!UPV+x6?>s94hr5P!QgF9 zQBs2Ut$ynjWmg9>K5B-XmG83x;7Ad5c(6K?343PsLdst;p6c708HY((%B5o;60#ItKt)Ykh z^Cg5api#;h7=xEF2LFswW@rCZfKp#kR*b1Y2Z4S+#O}n5&hGaJCB9(&<3`~ds?is$ z6#Ne)E~KCSIqu}C3C}j2{wWHKlb#4Aze#q!`K6aGUBWmhs;ZVOSv<_jVaD@)G%`!X zXr^Fo0ODk^X@*12rTbN+yj(|ygi%1!?oR@h(N6^`za!h(*#!k*G_x*-4vL4z7hinw zk53NPF$h+&HMej7*t1Y5_CBaP_Fa&|v89i)zMWF-+bgQ!m|@^o;^x zX#wH~N~$51E^4)3|I!DKa?=b1DRL;EAo})xxs6r|Zw_;Ymel9d_MR%UDIp!ARR+V-hi}$g#y1F)OyCANqs7m`TS=rY% zG?=WSqq{{k#YKK!8wlk<)3UHolv92?P5Qw)W+*Pmg%eax*G>9zJ|HP8BUGFjfHFdP z$J#+_bQ?_o_|dmCporC98;Fns&C$X#0@M%G6a<>Fa;fdCm2i2S;Qa_MTakdjcRjPV z`zt+ZxWkCCgYVbazk}&q!~{S?R-;%5Q}7**f*kaM9QZ;G20#uxAO{}!t~QjOlzJ;8 zqaeQ^Jt>v^0&I}8h>uT8tF4VEX(c-Gx8DW^!ZCSQl9+fdB=RV7wb&o9Kf6veXiz}` z#|*uu3lm(1#KcI(k{N{xP@j_@z~l-imzkd#0zaSvZ$5Tx$`p<{lcTetw4b{6%1XrW zTd?JODqgg-Y(#l4cPpKZt1HM|KH;u*Zgy?~UZfXZ6pLG1ImWWvY+Dp}2Eb?`37= z-nx^RnhQs5Bj;P1S=2yIy0nxw@}h3+g;7!2VO1t~vt~^q1yEW?SyR$6TH6I$pI^_! zCF%M*J0CwjFxbwH7_y7HE!mcWE8loVPD;~qs~MjW*y2elOMUbpV?RPuedadT z%_ibmvsbQJvZSF65PrAH?}wwicOE!k129)Es?S~fQi!kbGYi)0n5wEUr0(8SX520? zdiuGEbJ|1aM}y^knxtXVmE*vMwlnW8reT?%$tPA52RqhgJfammLnB_8i>*e`&j&bJ z!qE#wr0xFU2x!MOz$jK()OElDY0bVmW>Ew#wi*Ao$!w*-N96utVK67&5beHr5wmSI zzf{-1(xNpjvxPKvD@Dq>lB{y0u|uwKvP7u{x4>b(wkpuI+goZR-5qr$*e$G&%u>{e zJp;N;%_5PF1%8lPS?MDaD~(2)r`2B7;@F`cHFfGZyAECd(C63=xyvv&vDmW9FKFDv z@Blmes@4{x?bSU;?ml`{iN(CFwP8?SM=KlfEBn;Tn=^^sZ`lov%^kKf*rrm2DAZTw z;A~q*Nn2g5-5sg{etyAL9cHd5K-v)^P%Agz&(dTsxgOdAb9c% ztsycKHA!n~%E~4RgiaUUh4%paI!tJD_;q64o}Ye&q^;yQ5dVMJ2k7ruVTacXs0b+g zVC`B?zF21E;BE`UQ^OcLXtl4EE00GqUW^qnAno^<_-(kN@6hPTdC7(i5fO9eP6l(L zFZQVPU?zZ69fstnF-$K~Pf`FFZDlr+P=Z1%%#-{*ksBv6qIG<%Z>X=u20S&D+_i3~UDY;|^WU{DbvzL(J^ z5=8*4S%|>sCgm^bb)A3%nvJ#h;%K4`eSRU_&z78riQAq9WJ22vkZ0m(Av)37v**sC zjsjp5B`4Y(yW_Jk@YOGQ#l@Fl(zKauYY^*US?ADs7tXx{J9@)weQIp$Pj1$PVV2&5 zn@7YWhYtw|hy;G}0F?M7@(;!25KoF7cn^yNUpk)T(w8Iw0fR=5TsRpB-npKpA(6}i zW&seqQ$oVOC1Rv+b5Vlbw*|`->_2wpOdpM;tyADjk*i_AqgFAqH$-A0zisweC5Llc zl+o|Q;z5?f2Il7~om+7cBIrU-3f^f^F(ts17S{pTdjMvIgw>$?IAw=e3ZH>`=u#<% zDoVu7&K3c3z$L)xsYhySN0~1Uc3udW2mPlLg%Mwa5kCk`eF>WSEpHagScZ(qrPc<< z5d=tlA!=~I&Y-|9rLR$bHO+hgAqokN`N(I?h0TsgM?oHPSYvD`!3)uG7=kp)Z!~FO z{otD;6Ilum4mJvI8f&~@GSP%S#JFMYA}QiSv>1LW?}_FCa@0ogwqFp2rBw!L+hp;@ znY&2{!zSJS;W=pg53pzq14(;~(1rkcdlvft0~Yj(ND!+ABHL~h7D^bJLwOBSCd^;{%~A2!U*Sk;VF;qJ!j>aq;H?U3GS z@|q3WiGrXsFiOT6&4!U+tF2SyD|W?-716YA4BCBUOMF3eqIXxl{`$+vXSsu1Nh)2J zNCsnp1dM0U`hjF6CtYA&9Hk9e7xZ~CsfNJqqmBjIXXFG#>MJfrfQ7|7FO{@L^|%$1 zx&D3ozSvhIYbwz{RtGvmZEfJB&VlO3`jRG%1TXdV2PeSH?TKnFxwI2+(DwGFKm+tE z3?E_IJ~;}+H##2h21ULG2wVwFDA-THlPPx(uqepjAY~#U(*RGPI7x=AEgoi|m~(-) zhYBEwY{JGuJP5!ArZ~@ckr8TYNzp`FaDG(Oh@#~sD4mZ|`P2?-T2lfq<8|bAxgs0D zoVVeCjN2F+!MmdcaKQ1z2eBHs9z7ogxI_3XUSo|HgwKm5(ri0X|At~-FF~50 z_&2?2U;>%AW~UNfR}LT*YDwn`w|q)Bg@a=c_(yOp1uGh#PSZVf&~dyD|Fr~e&F)*5e}iTy*>zgkwiVnsbDrMrmUi1JK;H7m&c1d4!t=#FNz;h8p?;Pi z=fWA^*iO|X!6s%*ZH|@*p5@n;o2HL+(Nov5tk<|@VW43gwUe{zLBpe8qTttpsDZv# zEZiE=h!NvgJrRSK`?|C|K;e)c{3|J`PNb2@B`q#Y2lTC4+na_1O5ti^f>Hns7BbDh zEEcd8Kn#@C4DSXzs+m1a^eS@W>1^ya>5weJ=%BT=y)kq&v)1fpg1qGjw1wKo6mL<3 zq6x#8`2Dx5m`COrjLdqhEIY8WJOhjH8EiZ5f>oZ%q_E~w>7*+QeU4>a^!b^o4<4i< zv!g(tm60K69K?5XADxt%x(RZBSQnb_+UO5>k5N4y1iY) zbarqkV6S1N3K=IazkFcZ)~)+5S8y_)+3VJ=eJ#p$3!&`M;gi77zf7u`aAti^3&*U+ zK5*>H0qhH~3~9THiJwBB1pD#=6b5a{5ff{Du zaE?iQs9|$2-R0PNhbI^V`+~ZGW2}>U#DG0fWwXTvsCr#PLmg5$iuDzhm6Wc7y9z^1 zs+1Mso0?3VC9-RY2or}YX`|LfW$)tEtCtrNGwf8(uCBu5bOi;=BlxdQ5mpW-jOZ|o z=qQY+6GqgDV&_Ol2AoBSPdCxn?S7)R8x{ zj8%cZKW&!Rarkw=;MbkNuN#M7H;yp9ys5Yf0xBp_s!Z`8@=FDnWs$7qouSXo&B@8l z0B;CEgS7Othbh4F5L3E$A5@;Sw3Bk6wRCOl@L4@#h^)<)$Q4s;SUaiiK@1zlq)V)mIBlvytlGhIZ4m&9vF{p=rHoo#`FZ z+om<9*YO&Cl^*AqwwrF4Zkg_xvP>zabW@%w6Bk060IY{2I0Z!=TUst3i-|$b*#_7t z8<>_x4O^XjIgHL1W>p`ZblnE9cvip#KTS?EdoobZ$hbLHr_+70M@zye$BbRGX3TS> z2u%-@xDq>q*}(!1MpGl=bWrq0)R-{~Sa)Uxvl4ATbCF45N*M!F&or`91Vj|9JAIWN zSvRJVxyPJmj-Y+X>}IsgN6aRixgsW%oxsINckU!MHc`u>5JaU-12Xh>#!IbX9fm}4 zRwYfCikTIe< z{HZ!s4lAFoJW@sj8X!hRLPrXt1ZazRyQcUeMhO%6U?S->< zfoUK~(idB$mf#DuHg<{-rfn)ko>Kh7jyA;0+q#UM@Z~x?9%@NrPjWE3d;g~qP;GAm zfm}BZ93vSqlqa->xn^Wo@f+!zhg&*AKP@@!>ctUq2s9g@uys zYH72xx6#@Z$Slx#780=(uCP>AptT4b4nqebh*RIh4qf&bab_I%+kY{M$#g(N()gE2Bt|CH0N0u&O}1rKy4r>0|3Mf zA`d7?#oRhS4HoZUJ_OmpL8y2z4Uc-kZ1#Zh><+8=C4LUShTZ$yZ;|-Z5r**q#1CMJ>$vv3C)hwV1fHlv|-`w*^!K#u_0yn`yh)j zMg)AQ>!gshi{E)??p$2yLyoDd;goy!)Rcidor}`o9a^!(#?2{=!Y61JV{LN*<=!WI z_UIfd^vDE2CL+>Ki%|6irc4F2dJ>F7(j{=t;Adc#B2vJ2agNR^n3qnju1GqyMM)Q! zEY48&eg6F2=9xJPBQ_Z`a~fu52u3UfGt;%K;z7bU-+i$gp84)CzB_z)@7{wyz-aj4 z;5Qfs(8(%m_*{(Yg)7_EYkr z;v`TWl8TE^V#hk?GU8sBbL7~Dk#QNt$!^}fnW8U8Svh)IYfCR_?>Aa5@%_NZ3PzWD)by1*aIXZD3$MrR&Wo2DUr9Ysk$WUHdQj%}xbzJ^t%gH+e zuOl-H*}^EGRmX`al}szXT8zZfo(xFaBR?HY#Gu|gdxNqZGJZP>*6o|H{+EjjAN_b# z6Cvo#Mf@5jc4s6IIl5!!0)8dz$=23GJmlg-=(NfL&j07bpWt@BTsm;rh++NBtPX2u zb?_2_)j?A4+_;^bSJuj*U?f?o6-V+If#rghyan{|YbFfH{dJ+GVqMLFt!PfpN+7k2 z6Y_L+F~^uafG%=L72v2T%nqT=WkxXXV%7XcXzP&EGYAvhNOr-Z1YwvF4dzsRA}Q4) z*s3M{Uc#kBf$H)2!k)`#1*(U2zmO53!v}j=fXDDI{jYWNcZaS7Ey%m?{N-=G_11=! zbEko;^y=z$8&|E0fM$qAopMgO?!Co}7cc+d{dFIH2==f}ym47q*9YLU$B-o$6}SF@ zfuTXDI+fsJi9*JX4bct>3xQ_F zOQWJ(z%vOPHo)BjcLRov9Mji-R9Mi&@R3MW7#A^a_|Tz4BP04tBx9dNLIB6O_2>M} zALZxctgy1S>@*WWEC-DDM&XgpPE#lL2T^%?^>*M$4gQ&#M>GPnA3B@ni}gfc0{o$( zMGQqW=#3_cQI$;}QRPjIKYCz9{(!y;VCzzYH!mxzxQrILDJ!J}3+j0x!30Ixa`Ue^ znxL#;0@9-EsBzHQg)A8`q@)}zl@B)zD2W)1JzC(Vjg*1wQ`AzEaN#}{(OW0lP~c`2 zA1<`@p4V$a06v71!Si2x?X?NP!5)uc4U|btCx1u;;r{Y?%n^#Ay#OeMw(0yv7JFZF zGGHnxxn*sPb|}+Uk(Y9R#E6lqimT8yTd$T`jY>$U_Ub?J{S~CO-M)YLICTEN(D44& zHC64etcvLGQI(LOB@R(xlb(O!g%@T{4vQKdgqk-&Bf_FS3@5a5RF=<&QDN4|YPT64 zSPlknwpu-C65cy++VIK4`r*C9rmb2P32Q-kGdaXAm<)BL<79&d4GJ03+uKH~wCUwP zBqRumq|AS5gc-{qTNO)t8*WXS6z$uZ1lrZQ7?sr8lC)BuYVl+6x|LGQyLHmfl3KkZ zCr#4&<7pb)L-JA)HVs2r7Zj?=$)g+)>iZ*U1%=`aCGn2b+l0x7Ks<bCdBxcTc}s>y zyakQ82^w(|G~zf4N|?z9xTg=1Fbcv-AuS>X91hu($d^Uv@2MMpEc(G*OfKIN^&0Wp zk(-vC1z2MniovnWW@`!;N?D1Kdij9fwo|W#-)1Fv{opeQa0@*CxVX4quRm<>6#zKI zwV(LjrJv)bPoFkODRxf%@yC0PCdq*5Zf;=^t@mYi(2W(jcXrci+MCz|2V%qEw5<~# zyq#5E86k+=1pal4&-QBX)z{yS+4C`_UXG{KOL@Dzix$Rz1Wl?mU1lOlzQtr}<(TB{ z{g*}eSoSB$ZGI#_T>{T!Z(6-pCU2)eI6FClM`^9UDd?`oH)?Zum-A>kv* zZ79wd+L&eiwa-(27TQj*at~nFlaBzd?Z#j9@0}2=&xGf;Nzr#5{hkLg^Dx1V@Z>A$ z+9|<&QFnJ3t(jqut;0~h!p_sh*45R)(alL|=jstb5{34I#o9T+irQK>XeO$A#*y2w zXpcexAv;=X*SoiER&uF^Ccw$dXc5|jyXVdcH-A0$75~Apcts;EP0q6I-P;atGjWXn zD?P8v=gi%G2&ep1rX5H5FlGV=?&(7$)q<@hluScu*kYivv>@zL0s$QaT?8?w1@lTQ zOOVbEUM}c06##v=s(PYrzXK)Oem|TXA~w1^1c6jQuNFqt3S>lt;oyE zaeEl7=?|p;mx=IqW*X)SHv8r*6y$#te#Nr>|H~W`3v)~h9q1X3U;u>v@5=lORObJW zvrHn)GARF}_?3pgHyQrM%wk^g*N%higA?$SaQtCHJT)KwVs2SIJ-IMvQ2XJ}=SR(vE}pgGBITLg4-nbKgoA~Y4g8+0Rf@&!@LRe7L%#4Xa31|Gvlp- z8!GsgtgW7w^3BOEqEr%n4zk_Pizz{Z;h64jPApV8Z=vW^*n|D;jl^INRDnPb4J7*ZIr__&r&fkPu5i2QfMim>CYl28~(s zW+>q-GBZ00F+oK`=SL!9@+F^%$<48upoE~zN3%v$*WS>kkwip9SQ{Vo;GClPs)&eK zTRpAGTUK63xgPp5q>A4bQ>F`&jYT3(OcTRJoUMvh-F0-bq5KY&Bl5um{{2m69V0REA@s}>ZyIER0z&JwI^6s@hJ0AN>)xD*^=Y=gNXdU!QT7aJ(RbNk!+n0@Ay4!1TZu$F)+@|y3j~+3wtPi z$0rg4R+X)yi-^b~IZCUjd0bOpQC3}AslOj1ks|wsbZQkIE*{>_j_wYwDpZZ?dD8sL zXzj)rd{1P~gZm}aZX^Q=3#loA9YLuzl$Q%Oa4sSwfBr1FB$VY9p>zlS=}@AV7127q zPd!0II!3!Y8eZ-k7@Rr;+TZ3^Fo%Aa$j{?HNBOcKcu!;aiO7H50B`R7|K+{FDId}# zZZbkczQaEq9=20k*bug>^vw1DmWjTQiEzloFvvvD&Mria#gqn&sLcs&KyP|I7Df(^iB18d01+{SY9%6VC_e|d zXxIHuwoC+fX8{r)mSR*lK%gnAv7BFy9W)@SaQduQ^+lMMAsgNY0JMCSAKe$^6XK^0 zCA*b?a$G|uE?D@+>JK*1dQ~*D3bq;DbbKbq*%r~wj@nyeI9NevijsEiO46#K(PfG40v|NWi+oc(h22i-MIf;l1~ z420(zr(^cvhoFR1DER^I^JY5#@hnHYIi2}TOZ22Y&ZN5?Bl>~=2o{<3Dl5ICPcOWi zaSR@XF52`tp9OBpUDGf8aMK%%^*&uR>jNsmP%?|@Vm#T!%%HhCX{)}fsHHLk?xjxZ zuX3~Xa~(z>n?GwhK8s?W-THCOHoZg+F3 zpRf#NMsP!sGlRK4VMztb6V}yMQR;<()-*-m6&p}l`bjxKH)JvJm?g(y9Sk=w-3GJQT+&sKI-DosiO>3IM6hM8#K7aFq=5Y_gxYHeG-t!ib zAjnwI@AU~KGK?j~4SI^%080j03QWkHTnu{ws*Pu4=cb{?#c9OVC5;3*_NK0m_SV*} zwsu4}J1Lm#8t}yq)x*4wUcJ2A^WyjI+m}$-;tFrvrMV#a=Y2ar?yQ^n^+&tEy^0DE zG2y0aX!MaW=3Va(#+}C$JQsH`lz$tZWqCr}m$b&*D?(evYQIKy$h)R?PIN5cakmzh z-p9=J7m&f|RN6nl*VngC{{a*`@D-;5CrFj`9k(I^4jML@Av$J~eK>d%cgYbE26qKM zm~7^EGecsO8$VdR>NUWcudQ1B0mphxTQw1G-8BTBXnTPATo zQp5Xm?*A8WH;+#=#^?WM9sGZhb+8U2L-SVmL1V7N%vlG$J_bt&5a&|5KQDY|dP9mT zpcVk*;4x8}1icCG5IhLFV>(?F@t%CZ+?3UZbsMP@NJao53sD)QO0tx{$iylr%?6L@ zlX!zrD2marP_SCt%p5WRY3ZYSA^g<4pZM0TeGve)Y0PbvXUyk69{VZo%xg^ksh{j>f+^B&L7FhRNi&4(P1oX~7boqpyP~C3WQOAV#BiYBi ziP^mwFf*by$|WJTiq6^wk^Sg!UrxDm=k{-t($kXSZ|mE2%C^GW@k!YG?%VAPXP--p)%t0fBsy-j~EXbT>=?>1u{AwGCCeI z>JMq8zmRh9XnG!{*Fm9!%!7YD^-VLgQG5iX3Ycd|K?7Vvk?n2ADc>o{*5i zczb)phT8sJLP8r_F@-RGk8s6x~oKE~j5vU3pH7i%JrkaARqJ?AqomoZM z_mH&%$F%l(Y}SVS3;>q`M2WKh6}LEX7o~r43SjL()w^gtM|%}^W&>Ri7^8qD+5k!S z|Fbum*V_K*_fbR)cHi&yn}~xUC+YOn`*-dpCue15rDv3uo;gGIaxqMNJSZX6#doe< zORX#+*f->{mQi>k$f(%idG_4(l$;x>MWu)vNV2l7Tp^$142%h_s8vGFvag4Pgu@xn zLv<|C!OVj73 z+`IoMx40z#5&A>ey9mY3Vr=5$FQqgndk-4qZ_|`|2}7kw&da!Rc>n%i;}cUjMM_e_ zulx7!`{jO4vW`LIkYCW(SYL|8xv8bCsXTW>TpVJD&kPP8)XN{6IG^qH$>CqlUi{(v zqel)OK8%g3H4!Ew*br0r_eA?oocJmN?&&n%(`3|!!^z0QCX(-r_VSua%e*9!c8qyD z(B+-tsa{^syhbt59MY;ok=;^ zEfvkW0VN`XpjqQmJ}r<#Tt@g2T4&vQtZ7MWDr6* zv0@{t2iLMm@kt!Zv~Hy#rA_QRs7KC}0-5*$@`)1L?_%D(i>>6lL}C*QiZur{iE~9* z!>p|AVnhbGfr2)sY@6r3#pF5f-`O^f*o#0=!@w-i-7wEE!{92ZX;wJ?q1Z1y0Q11V zqRTPxppa>aFf844lZKkWfMp0#m6fF5#;vf<&|(C`%E>95mhVBy#tR0U=bwLmdhc5O zx#P%rB|maLVzH>LtWzYx*hc%UC-NaATS5w)#`ZD}-eb|zDu7}4(j#8iw3Zex{= zOsrMYWPbcZ62G!z?P;$Z_leBXnM%5e_efr0KUEytM4X?&HAd4c{) z48s!2POIQwib19@J27x|4046pVtsrJQj0*3_M!K}XG?IMCwp%``D_i)msk;sk^hD} ze;=}eixr;I0FW_Ps-d4wHK1kBr&~ieiA};8o5iD$Cwm9#)1g_5Y)KP&1CMu~(u!GR zY2M*Bixr-pbU%qOuq&K;SSvob-auS01lRMyukpe4)Hy|kKv!U%fwGs2$JyD%MM%M9 zAwgTshkb50!-W{=c&53Y21efzJE;@G%0v0LB)j&E4k24O_AHsc6&v@?+ZqZ3S`r*C zL=>+v4z6Law@xyQ40R@$4!>~x^*#B9t#2Wr^g|59W2)#H|1!WV6Xh zP3}TkBU3QE^9xYJ2g4aF0Zk8Pl|}*!8-li3x?{wmq|20Jh(tWQ3}a|$sIzFtj{3|i zzx{UR*E0a1M65fqf9yLu!HT(7@2q2@OUK(ciW;Z)TBh7{%we)RisYzJ~G}fCX z5s(B&HB<_{EX82V2l|Hu6B84W>J|{!v$K&r1E(hYycl)Skj1Wm=|^}Y>4FIQ*xAWz z^YY{_bp^TgB1_lH59(CCh71o54nA_^=!Hl3FJ4C3QJK%4{iqlJDYiup;n&-i_U_%= zx48`&y}Tjex6aNVKZ-=c6m8fPMr%`AicZLFZE0z(FG$TWyh&}7SjD^V&h}_)Yr~>B z726buH>`LQFsrwG(o_@*a97$|(=|;gr~4J!%Dkq@<*&X1Z4)#|EA03U2FxFSpuHaZ zHqlIWtUKk%7l9vEep`$zP%)B>(Gi3Z6R1JrgW5o}+B~pCVoN)m;<6CO z7&$!=r!vfDDeXp_&B2X<&S2S|dFT%rUF>bH-QbUTI7ZmdN9cGc7y9L7A{Jq6J3&wZ zU(?_)jcOF+UlfUn2*XcJPe#Vk_z-MuFL=3~xx*IzKSXoV(Vvmi8?d6lELlixqh$1FC2499Ht*EfH z=!OU)!f0hQ|HQ_@wzv6DVO~;=PqF(a8XxNoG7um+BG|h>5}8Jneq=}!S+5}@P=KE~ zZWGEc)QDi-KEh@~A}4=U2r1MES5kQYbH$P2SJR4$=t-6d`cc?x-HS9~m0^>J4ZSNN zkLaBf3E^VRy(Ysqd9q?XIX{NYfMtO9iOeT|V?Mdke6l}1S@`TfKR4Ta?nv{wht219 zn$PY1`?*iPH;rCA`CQ?94?HD&ZzG5Vu`x_B){j&4t27fay>q6Q`{!Cq*Lex9@MNv! zuzQn^BenEkO%I9m;M}vy)7AOk&q$(g5$X~3rjO$3!T&#=fohfys`7V0q&R%hQa{i(q}yT&A3q6qLim zc2AGe(y@z_{+OzhAQuuAE?a>f)+`CmKTblKA?S=4 zE*gNsjvh#Gd(YoyECS0k`;vv=p!E!BrArD)QVgiNERN6w4yE^M4T8>-?JLg zTH+gtd53gbOMEV_6-4>_4|9#mIyLay~b}U#mK}$l}?(8z+wXtI2av3moffY2o22&f>z%mL|d3MXF~a95;(sFCpSj}4}P|1 zbFgRmcO;un7BscBP=?)G)?4@Q-Td(pgj9}vym|ju+o{F*-FHKVK=S1oQ>J8KDGZp` zGxFj^fg~NwE{eXv8u(Y46am2xV26<;z1!fyl*a(7cI=(HlT@mKT4%Qe(JX*k z1Cmpb!P5wuH!9T9^5X_@$!oBak>C|qL$a!oAdj9- zplOjoN;7<59Bc`q83Ue3Uv)upj}OSlKA!Hh{J0w1)OdKfyHkkJ)g992j#PSJF&=;W z%4Qj)k-TA$n+WLHCvkTtBzzR;A7`iMBN&lWX(%sxSY4fz1f2us2`SQ9TB{|nTimP( zK+s~XYReXTduvbbBcBs-SF>;Ho9p3^Nsw=P4f4A+9LdR+%-89@`A{fby6xtqQ*`;SGm$g zskE_dH;P(XoSliK@J=YaM|fwWwRL09Y#M@Ip=^Xn=#`#a8RACgNHJ>kR5q3rmOnO> zmp0cmRDtM-I{a5T?USx?gT45B*NBcB88XDY3!_NHThnQ?QEvbB{*!mkr5cJt`PU@n z<*2m3o!ddX$&Q@+lWu}^LNn4Dbo)htflcSapg%GsX^Wx;k41eV4fFnjNTj&LqQ#Jy zm{5|!?V#N?R+RlO-Q`9_PYh4o*et?!wFne9wzYIA^m^2c>)>`+HX1sg>M}Qff0b&) zh{1!s`-F}kHZpYNh%qDm`vzNEM@N&9!n@O+>Na6@d=b6&B6{sj^xBKiK`%lF^?`j@ zR+^iW@*o|+PhnwJZcYKTj<6H8KK(Ivn$814hgcV+E5<@B;2gjQ#GH)fsS@H@Y!4v= z#3(|t5vWuWF-OGOJ|Nw@i`%M;?q9!tefsq2$aqTl^2;wrk3Mmt|10NyeW3E|zwKw1 z1##msu4?K;$%69ox=s@(Gd{Bbw;@JO^!PE&VP5j~+w(c*?e{ovounYQ->wUaj0DJf z9GhFANx?Z$3>^znJIV*aDK0G))Q_w(es8|O&U28_(+V!iAyC^9h6)G zsr_EucHH9baa0E`iy^x?k;m@cZXDT-T_2IdutrO}CI|b|)Hfit14K6@*A+ui2zyNH zt1_{NbZHgPYJ-8?#kGwBYyud_NXz(At-+W$5q5f~p`$Jo2Skx$Sh(6g@Etu+B~UC! zKmn-@PImTIP!SB%r%$J9l2WbimF70c!$YVOh^so$y=_enPPl4v8h(FbTJpbKHIE~C zW-RwrjPI)$-&l>C#!V#Nsd-Xp||7*G-r?W7$Z$8S(+~yYEh$hE?RVzmNo-csoeVaPYWcJ1guAjq12#GeR> zPJYK|yz|G8_@|^qLsGVSBg*ald({Jsu$ZlM^?AIO!c>yFq>H^#~&Mqx212;?mH zdeB*$eD>_wtMQi%hD+_Jjo{w-z;NUI-o5)yBqY>d)^eDC>(w!zr{;nQRy{$ z%oy1%!-)%b!;l;|OR}}f*B2T|-xdFD_lWbV}&+lT}3+{z~yc$9BvpWq)6)+80NK3w1nBh&^uJBuhArm7SYa zXC-tQ_G&Ta{6E&-12BqeeIGuvJKM5Jc2hRJ_t1odDgr44q>6|gka8_3wyR>fdfC|x zps3e+?FA7P6%Z*FI-!?@5=bwb-aAP)$!7O^-kA+W@Luol|IH*jv*pa3Ip;m+y!Cl^ zIK#t~0N&}gLi42cj=1^uyXUe_#jvT%C1h~;uxX!}6xO(I-8yY5mOg>gY0cqS#o&ft zi99dWfQZL=6*MPMQXw=2sWj^=N>3PdaLIDW<+dK;MHn>PFb!fRR>^E6JqT$1a3IYA zFvDAC@bdBZhw21ADp25hdLtkO2{r?~bn$=TD*r*Q`h{egEP5GDFJda41VNI-{#34- za!8b`!e9}ETvd$mc$x&NVpc$hQdTq|VQN65BpY8XqZ&C2#FZE6mjoSy3;5syo9X>J z%-}dwB0B3@`S_ahvPPCqKkE@oCpn%lvSsu>2rPnlsBxCogfS7dl62CVFd5D$REyv95Vk z%VG2)(gvtRq$Z*b%^!M}8kajIYE8H-^zmJE%fCZt{EY7A3I+p*QkVh3Mo~5=YF)Yp z<+2#iU>Ic=;pwu<2&ILQMNt9DFpEPCQjy{i&Thfs&rA0p*o{hWFoLS_`&Dcf9gaz* zLs^2e^Z;#NeQ&Y;L1iLs2QUql+buSc>;ij>-pSx(;;cLMluMXoBT<94)PRYg$CuON zL;Bq+JwIGu@$~R>@TRB7@fbb+Goqc?t&=EG5qJ!N z)8Yf}_%NpZmjuG}FeW*k6v<$!u-YiQ&j!+a6=!lgiDo$Zxw6^9YUx>K;ad0MTFY^* znV|bwjAPLNi7kP-t~5LE#OeIJqN2LS;&L!N7t4xkD+}P6b>?*51*{*+9)$wAV0b~( z3!7$8kei3TSK#C}L8I*I?y0SXNmH4wYQ*fUX=tz}Ip(>Y`u&%^zaPmserRiC`rB{A z6Tl^K@2SjVCwKn3{nw+2$TmXhql`RpB9a{*fya4NwdR8_w{Ktf;Tl41=VJE?niY%g zntk)lGwyg`MLO5onhcb;R(a>};rcXA1Bv$=_7z-HKUO4796vhYETM`KYM4cpH-5tO z`-))#-v~b8~lZua$SWyHKX-=7!!*PH>lX^|fB2>?=x5zZ_zQQq|W7cD@%a(Ao;< z#7ix$O6Z6cXdxsp_4d}dYJ1b8-Q03>Ax3vNdTo6@y62la9F>3DC${MS+zP7#BIeU?8LRh9N(2FFS5dPz!AXzPz zJxdw#`j8*FR;A#2D#>p-ucAk$Qo9ZhHUx$1R0;*F5>!?zTtsXg-KHR0cY8-iFD-^W z?KU+-HrXyX_W=0VibMJ!Pqm7G;y8D^j zyz_8uve*=1Y4_ZBI|J2rofF4T&C=;{ScT}BGbgQX)Flo z4{Tj+9X7QJR)&s}z2AIeo(xr;q{Mq#w%?XG8SBhuaerU)w@H)6M!4G8q)F+6Htfp- z8)hj*0^)-Q_U`#**WLriipWQ~w)oiI-+tTq`Hw$+w{F`m0OkX}CB{_xl%njU=E7xc#vRDY^ z`)l>R`r7JdKqdDJ;m}R>_VezpqG3kq=BDlk3#;etU3&q zgs>|!`2d$-l))p2COg>(x&YTS3H%t|8R+H!pW(``tYX*S8>o*|Q+Hsf;!uX9A2_r^ z%ETdz4aVWm>wS|{Hb5$SK`INsXewKCP1!%Lz_id)0x06i94qL=3)8N+`~tO-%KIaZ zMY{J)^c`hJq|tgSRbf1tU!+JZIK zde8oKQ~8&k6-u(ZyO5J4CrFAI1Ix z%SczG`IbSZPGK8pSG>G*dPGD-8gd8Go(b^H&t)rCteDEp758!IL?p>#?{Y6amJCVs zJ=w}7;7|f^M~^Xg3EYFv!s(oL?%4c-B0S;C?oN^|Z{ZPtEkPbZjbIc%_;IZO?RFFS|a zH(fp5WahwTmGMl08I$@;r8W<;*}S|+e~fIXh^038dMMR;Z%*#!M~G5@VhagUX?S=b zdx6K2t_mn%IVEvAGq(f|1|PEbm9eq9a9wO{GF;z2oWrtp90N2y2-mh=k0E0 zed$3Ob(a^VM()R&rFGMziGv_ItCf)k54f)xS&Z4;N5k_NVha58yw=Ce;-yt=2HNjl zwBItc-(>JDlQCkVnp$crpclGu0RY5V2p1wp z-PO=xrd9yymS1xsO;w^Oqh!)Z>KaF0@i>mDm&pzv<;fp zt>U{)>;UcDl8GtMz#ilxFi^D?@rdY+RF_f;q00Y@)SELO0DW7$LpY={{GXap_a0_x~c%C zp?7r+Ku4(*kp{igQLDXr-Q0TFHNB;|(p&S;Tg%Z~&!D&Fp|?l_Zt|4%+rYC@@-rEd z#rJf<=|%A9?zV^fA}?KkS#D2A2r?R21*6J-mImipyP&aG!kx0Sv!cQ-=sP=EXGTU# zXJ<=BMrUS5My6Q_edV6txBTD?KsNEVf-S$lnTkk9F|`=TU@58_{?9v~w;l$q0=)#f@|%v3&l;haS08l??aQFS(>7 zQD2U3Jc?V!_+iS?!I@eU)9~|`*naDX5##{S`%;j5eEFq^zjy79QyC6qamMePkP63*e z_}13ex{gZ>9RZ_UbL*v!?p`bwt*t#h?PpF@UAiO>_3u7^9)5SVxN14R!d5P5>Ujl# zm^u`SdO_aN0k;QvPe%vbS~{u(Rwd1GkD=F1==Jx|>&wvV%h2m1u^=7avt!q>tSvCS zig|m17=8j9u`s2jNEVn4%I&DjLbk{4KLC+r^JZ{Sc#7Iu^mJQu!?i@e_<)cpQi z*(b`9!RgH7H*S3EEs(~;g3+a`AY%{G86L@KyM{W_)d1re#jYn2e!_$a6K|i2C9s4W zgeZ*9SsCp-7>3C&Pr(PTVKi5Ar2UnPvjH(f`T5J1jT)5<=jSf&fx%N@)%q4X(f4qX zLU19?tIQusXe2}r|G43C zeh63%2!MB&O5vdo4u-my^yzhVzP_%m27~)0JBGbJYN%It_n<+@R%LW)ZR_an>FaaI zs;(iabafw*+4Xca5-xt-ZbAC4DmM(uGMGNfsw$CVy0MWsr`fZ~0@Bb>d0^MY>gr2+ zP2HhGV6!l%x%3$#{P_-0VTnj(I2EZZUEKlLId(m%NgVwvptV(?wa>%ku=kU`kYkfB zmcj9=yr#N1@66%DhtK5I)j3bB);#3=J$(3NHhq?T5~oMA@`_9G>q2l@Fv(?QozE?T zvU7tV5W(1N=5T8pS#YowUO1H@ZpY4?%gH);@E|1Jwo5&I_Oy(J?2S; zt?u9l-C7F#CxKO(1pABM#AlyJpP_nCF2 z$F|~!%#r1A$ol$Z4)d^~Gz&mOg%=){1>$?9O>%RQ&c&cq`bHj*7}1@V*By}ELoE9 z&4`?Du`-%GT{JzV1y#5;5DH5npX8|KC$TqFeqfNRs{`4jwpz4zc|~3O1!CE9E@Wk8 zUj!48n|=Cpc1bCnUC7SLxIu9BsT&x(!tBj>HtulZ%Bx5fO!%RKF-_QUC9rTw#LRGVb2PMtM+4yk2vC(zdIDDO$5ao&`4;hASwzQ$amoANsd_wjFl$wHfN1H>7zz}TY z)kj-D+(v!6Tf6#PutJ80hHBsk(oH`A+P_&1$01thx_YD;QUR*HUf=vPX>R|Gmn8TI zt)||XwxC`7GQ?`C!Lfs z1n^TqiZE@bR_ASN!=Wc!+@UP`sY0*T1~j) zC_k0TYf(Ov3I<`c$t3$xN&RArKkO2;Asa9JZ+TpwA?jMd%Z@+ zQ=-fokupKYB3*h*l+pWN=rT&u$fSrGnIyJ~lDMMEnmWcbq%wA%0NQ`y~Yq~N0H>jP_v zKMuajlxgN=A|h4mXxQ*ry?YI}`w_P(v_uv$r&Wooi^dBU7z8Iy5vgGAufnd^e)Q zf@0wl9~%@F4Qko9p;L*S6^`^QKb_)RmTyr7T0Evz;sIp%O5V|)eBIs1&8&5$w) z=kf#!v z_HFs`yO#QvlJVbe-1yz3?CLh_PrvNSMChxLtrwKTb8?2WlL>WV`nx zC)5j$7hrnWEom@P&==%OcMtj^1$~i>z6j0BD=W)Hyb>bFFP=G*m6@57lYJ)h6gi0@ zHWNPP@}B7E9>JHCeCM1>K4g1>lCQ{8)85l#4qdk{I=VxRTpa-hMQ7Q`gNJ8Ki*eSZBByHUr=7D+P%TgBgpg3QcL(9wa z^P9?Hz_3`V8aj{_s;{%7y$KGXKxJ-i!pa@2tEfO8FH*|&JC$;xTF#0kS*g>RL-l%< z%HRu7Z@q7Tm&V1*msh%a`63+x`GNXkUUqQ{4tA3ia59sQ6mY5z%qGoUGA&YLNHc&N z+T9K9Za}-cq21jWr&NG5#-_0FIegAxm`uZB1;xnB4VfDXjk4!2BGcNYNtEtu68_y` zMOw@}U@=u#Z1uMK4x_BEwN6mz6Jru%?!)Zi-XTaF#6Ywbc}osNYx(6N=~7`{>4ggy zN#F&P_}To@+_TxFCgZ~mN_(FG#~+ir+XgqDUZ&#Gm4&A-mXucqr9kWcyyH0~`FDM{ zVFP6@KC8-Eg$Q)a0gniZC$h7S96Ba&-fEM&ac?h^Z)cNFrYjdLxZ{@bbN8k|Z@v{O zs!f~H@C+tsn&~w|HoWlpWXNy(Fp9RrZhAjoZ8wZrB5*(MG9|Hnin;a7emiED#jG>k z58AZKs@gL6>yoSpgzxs+vO4r!C8}Q9&?{32uyC2w8X&jH)vU*n4qal{$l*S^prAm# zJ|<>xOw1Uqi@#qm!Wk52=O&MjqY_Of8A=Fp4eN1QsOF-FNVoSm+HEd+h>-VVJ6ap- zs;f)OD?vLhzTn+`Tsj&6H39g*+J=@6IRRO^$)M|uw%HtV4f3nnyMdXlbYmTT;N8_+ zH;kpd48j~#z^-bWITlu6c|h1ZZ@>RtF6W)Hc=6(7Z!YJ%_uhUdEI`)M!u3{R1XT56 zObaT%L1BoY%S9ez(E*$P^a6{AKtxSwQr$lYPFoh(IQSkbQ>)SyA&vWX9yyl+gW)&K z$lnp;-S%yYlqN&*ZM$dWoR!J=;wSg)%(LZuNL&j1u+oOW;x8zoW-gpDaqh*qn4M|WpuuiO;_$kxH!kZHBG)2>0fNWLHPs6xvw z^{UOrK)tXIZF4$p(LAtBE&%|qRy)J348{oq!R^w_PnQ1K?!ag;vShctVk=!xHWxT^(Vw|Fp ztChykpOqXVmUI_Ot`|#|iY0HkT=K^9f+$-d+ajheq)`;W-r*&DKwh9SGEm;NF(rEh zL~)>i$U7HNfq^KN5%@jDoRXI~`9nk8X1*5?-dPPig{sn$v&W8R6z1h*?%#R{9EQi4pX+tiSy7Z8@#CiTc+gJR;NLj8YDhK^_pxmCfZmTYZe?6Zj- zg)S6`g>T89OiP3Q|6lCm)xJ%X6Aa;w1tn!zH2M*w-mCQKg$I|B??VF89+Sq=t7`i7 z>#u)pRIT>ay1Kf#W98H$mxduE8kz>V%uNqltn=4qb+4-9Tw&q4cGYT8@|c5GA(W+u z(JF+_eLGqu_AC@GXWw)`i@>(+Bn)J9{bviIp-=QI?4aw~(6> zfOLk6^wbnpw^U(yJA3+Q#ycPVxPO3EPLDml_fRqC{r0Qoe{DXV)rUCCzO3V$|7Cjh zZG)^hW6$v&Uw!)Nr(f*=QwF*Zi|PI4(~MKw{{8QNe_yB?u^6k}-4jwCn(1Px$EggOjy;|FkpUN6*hVSAcNmyAGu#Rb|{yRnY;wPi%TYh z(St70khoyQKmI{huGb`+6n~$;vV9BT1=0QwrD@ITH#TH75Ae?B*vQcn2P>P~`vZn2 zM2;MVRIH;$Mvh1bmm?44;H0sULr1}iGioS$Ux=v4mrs8%rpF483cXujeGQf}j?4XI zM|Y?v*2jLX&)Sf;>}7#tZJdDAdNLI9{MHZC?g#xG>h@S)Mc;e!SZ3J(Dz z8U&|S?4Y;%>|DZ4A#Ul3T|SU-e7bTrZrqr2S^Vkg$lJaydT3&-Mv{DX z)ikMN6NW~AwLP!hijE#Jdv+MJz)o#pVYAh0cXyS@;M7}OW5rY6rHIiFOGLo`6Yr`9>*O*cfDI>{7TiTWW_f5WRu@sd5 z_{WL`i3Z-B*hwDlZJlr>b@W`8nM!gRJmSN>Ix8zAajC4Lqq5rO9~I}(kY@o|XuG;> zjE6l6TFEXUqSx9da!a0}-8(+XxS2Ss=jxQ?$2biM{xS6?sRV1#K!2YPvcUOanx@OZcm9(^pq9Fxo}Fz{mXNKwzIRb+5*r- zOLb%CUz->bbGtDHx^sVh?X}l_{mYZYh&%3xR#d?^x=Im!$6uTu&pN6HH=o5coJ`fq;uf@L!&6 z;$Vjcb`Wl4@9+L=6OXudxMSWigz+pW>?-b@5pynAkdI3a@INw6oXcaEdG@#?>yNCj zGRmGgIu0Hd5+lzYAD0TV$vFJGfjfSV8^0CYIz^kEg#@LrGX=1v{QRN>c~g5SlV-da z-1#QVc%wkIkyy1Nv1)nj`Z*g}oJH@NoSfowS;*&XlxtaKg`m#JV3j2$85x!4WQWc% zmm5W?oHZNCm~{O3)-g!&{E89QX7&r6aA^n9Wl33C0!uQdaU)^+@nK>*XPAAaqL$y{ zlMmQBXzN|MRPf@pP-{=tA1Q!ce&R%d?J|4JosWb+2zQ`|s4s6J#T0+rWajFYE`u0H z5iTAJBYgv7AtVI^qyz*6x&?r*;W>|1TYtLjl2R$KOU}9dB|Pbec+$;y(wFe0FX2fi z0`&@Nb~q48P%A*BL^40*@hhM#qSy+7!~)1!`VBbC;v&MG1%uC; zufq?*tmX*Qg?dd0#_UGDhYjD@q4WVdt%$-6jX1A1%hr7i=MLZ@I9Bk&R|hP8CU?%> zQC)dz+t#1H_~^TL1PIQH9rIn^JJ^SaCIeUT+b7U;IG&fiacuo$u+<1lz`jzw?a&!3 z5Iu_^t*j8V-PY#1HV~f8R=qwMP^iyy8_G=ac9#%WUug7G(h4#$*vn$>dH(qo_r{cL zKW}~NsXImtwr*LydiA<9Z4pnI*^0@C|9?wzEaG2&Y~chi6UQY_W$F0nH~1q-$+R}f zUViwA6fFNR(P->|ne(7iGRK8f>@B5`+_ z>pf0ejQk$gvhR`{^UV{ht82(f2!11?Ln81qkxLL+AF3%cD7GqK&s0>1_ zfZzrstH5S~k`>W!Fk!4hoLN7aDFAvwCJW?Y4dyr?bBIuU_(tZiT7ldv0_;N}XtSCp z0u2ntLmzJmF;xdBPGlw&J);2BhRpyR8!s=)Mu1=Xd3giW009xATcS?SV*c-UmPYMz z&@#pMz6x4i4q9FgS{?<@V`L2#Gu=WiDxj2<2=A3Bn@CwCaYE9Dms}oly#OJj81VtF zHlC;y8{$o6CV(-ZO-uwNi?oK6gQ3qZQ;wkyk=&r9< zVS9h`zMLKioz6HE&AR>dad9!x%?E%IC8bB6t2P}ev*aHC8O9FR--eBul$x44{-!~} z-YOFxoU-jgr^&79`Suh^*mRj>h*RXjKOtlPZgVnR58igYH5BVz zAe<4r;%2)VxO3%;*^2 zI)eREaGksDJF*;q^yca>f2lC>C*U9EZBlwf$E{r(hfEk^oVfhKxB^(wi3;bt2z%Eb z^O`z!YD&@+^QfBIN@Of0w7+u5=hc+T5<$KaEfVpGLLYR!fqMyln={tlxU4M1D1vpxb^(5u z7lgph%hahu@IT=`ER)KAqicFJ3fIlcY7&wluA@4;P`TM>9ZLCmB+7C42s1OEF7x$yeyfS?&9?sFVZsa9=1dgKVi zt|WUGD)#KNCf(q%Vcw0KA^04H>O90tY2pp>(UD#P56hTZY0@dZLXe{UD1NZH(K}>R zyg8YB7rRn99T5};ZV=+l!^K?%5175|0~vDTN_m&9tPROQpoHR|SoHAYugpUrmx1%= zhG8GU1p?Qth8<%@u6NtPAM6O=Z!-aM|o9g(-N3Ses_GOelt6JB3e z+tyKERR)B%7L1rCU>#5rJ36bcL#QdHQADE{VPi1q$lfesN%P3V!6WT4-&#^w*C+7( z2(5OQbPxsLCMnS`7Ie-$E&|{>KEdJ^1mH^W+F;rO{Qbfs1Ar9o1K4TEAOJ?7fI2!0 z@JId@WM4+)%m49~A}=)ylxqg%E&%1u0_Bo@D!vp*J7s0%1a}WbEag^)n`tp-L|Wgm zq~~M4EWpB!CEX0pUq;g*pAJ?P^eVU#PA-HVxU0iOYc-d-E!pAa80NZt`}PJ*iM_oY z7tj1K1x~{w@G&;C=yyp{cfLQ&rk%$f@8DVp-uLEFl=mRDJ$ z1b4roaoVClmz$R^UAlOY&5<5oR#}B5Qp~r2#wtgkF1$=DFrCqIiWRg5lU+5|QLLb5 zmDDVKwsg@W0a=g1-gAJ|rzYh2)$txc@}cA+NO1uH42cdgYhm*sus1I(ioTf5d|X)>yB7Vs0sSkWe+BgKq@r>_C5!2^C^WX9xU8}QT~4sbKv)u^I?&Tt6vd5J zJ`e+aQh=2Z{^N{XD!hQfCkM&}t@NA?*q?A|7kb zuz6Tc{ff95%5aKZzWP`8u3(%X(?(62F>7dlPPSJstRd&l96z*k=LO_Ak#w?j~HZiCYny*wFQowS>^&!!FTr)H5Sq%BAT6DWZv{{T?*MIzf4 zVY8K=$T)JUu+1&BY(ts*O~Ye{MD^t4@_cc&jqfhX$|~tJDYc{< za7f#Fr(2?8qjgB&&5r>z&O8Ysgav~Uuds?#Rim%aW0-BKz_OK#yc}o^YpS@>!U=LD zLWF`5AiGrzlqYxrHW`VciSf2Mcp%gQRmp+mzwkl^!)w3>p1=@TEUu0ZEypw|DV z3W$^(1xlV0hK!zJaux+jj)EhGo}l31lO%a6ku0GX=@%&y0MeV6UtDl;AAQF-ggGJf z7L$_P0T{@7qp`lN4U>3FM?0M#OM~0rZ0EjGdE(u70Zj3c;PU$GAoB9uON#ISliP|F z&#$;uacT3a*N;t@aLmkf$eK6z)|)Udhuk!k&SzkB?GSPQD4*CS;FAOXp3`s|BU>)G zJpMTG(@kAaq?(+a?6M)RrYqvE$3st?2sP`)uC$a_0cW?irlPtb0}MB2Km=q#r;Zt- zpJV6+v^H`M0AknA*UKj`;`-l69LbGs*v(=TiEK9eimb=)1H=IbjTn}TkQTCN4|tVa zZ-*{mM*8(Ac90GqN%5DlQV^%FXqEo+vX|I5RA@G^C2{yf@|Dm^7VWHDV8NlzF4FVbfHU)-+L@Qi&XdZPN@ZYA_n4sNjCdSKw*M0cSlR zF;^M6Tu8gmNR1ZnymKVRQVdpw!B`ate=i2Bf=9O%goin#p|!coYK6LVuOI-pTdl{! z1D^R(Z@NdW0}39bi{0__#vi`h`fDa=L3i`bf$bq7t$;J`>h2mj_GYs>!3SdL%9Sf$ zNJ3_)7qF%xZJ+$56t+|lIE745>%)+LdDu{yJl&%p8^aXKOi5Yg#q%f5May_G6kOO zT92l^Lb`yu;$bKi_NS2e zAW_Rf7Xg^d|uAQgr8@We6m zVCZ@L&hcZ$CnhE)j7b~;$Mc)VKP<=|hDAqUj(^F*K0IX+u2IK5h^Cf9k|i8Xl)cjNLVt5kXW06 z8D`+=ueJM$SaRfGNNC}pDiHziGM3!HU0pz1m;J`Low%8JSMtfhU9pDAl36b9Ym~J} zw{2m#tu&Jl#A9EDhzO+-;KLp>CON|KqzrJDPvd+V&c(|Zlek2j%UgbDX7L!!?jL9e z(w(}aP2JI^gyF1Z99=CLroeD8+PT6STrU|hUem;@uQ8@j#kfRVb1}ZdHO;8>Eb%n@ zezfpA!WyZb1J8PmFXXi?Os-2}3(KVzKJ=GcSP)zI255Z)cm0zVhA!2CyGkv5k&~l^ zdocXbvHw#G!|UN*w8sdsg>f0Q2A#`$4zV}?*un#JB0~!+(83C|uoFD7S0E!Xghaf| z71E`48~FXTGnPn4{}Ym;2-GN+kkR!;w8lY#G{@ce9_#6MB2dM0IxC@<5NC-S^AG(Z zVzbo`zg;DFtrsyH;DvjD8k9p>Y20d^V@-<|ptT90LhwcF|7ANmV>u{?4?9_pu?}h~ zd159=sE9t}PiBdq@myRyyoT0@GY%o(lOK*AS0XTAQZn+aEJKe(2(K9xxZL|Y zaP5V7Bf}+=fn2u>T{NA(#C6~Q$oZub*Pe4bdx1WOw=iJ{4$Lo?kq&Tm{Q5TP)v%LT ziZPRvJb_B~(#W_g#!W<>Of6MmwB=!)UfzKU}0CaE_KG=!t=N^FQ>&L(~K42_l_=zG$UBKws3-5q;q| z&>91MK{zW{#vb*B6*6l&!1Baw7tY?WID2CNOY)i-RwpvJy9}-d9 z2qYPtwYK^Bvn1iw|1?jCtFMUQ@&pl>coye;E^3YId7;ngcjB^@OlLmya`ROs25@Sw zZNE$GF0`LFDlOjU8fqT6&$T092VEH8uUHw^(kVIA&;XF0W(w-|09xu<^wbp4*A&LF zNMy)?rk&Ic1=1yh5l69t|dDHXt*LE8YC6+$d>^O@PM6i0wV z@0h0%UfV|+yyG7VDad2#*HL3;FQjo{oE9Nh`Ou)RmP2*9jJ&ZjkV?YJxp6_< zv(L?f%H>ylUki{L!F=0(JmoL%P>3f+Fo%PVTd zP3CKS{QiV#K1bcFBgM=t4njlb;Oj!j0DtP*P!Gb2Whux_)Cb=GItI#|M z?J-A^J=5St(zy<5ambL8h6&s~HW7e|#s1f;h~xEk^xi$_z30(;mva{uU(6BhB+yb= zka@BgCP52S6{6NC4@N^Iv4#^GLF>!At$lV<5Mf<5c|o>CszP+69N>jyLV?C#Ksc~p zaDL{AbGeo-`tnlI#oxdF{Ns@m8Z)=SZ{ly#u6^TRPC<=)_3GGIpd!n+>*f6>h21Tt z;F8?r_xvH8 z%3O!0zDNcT+P`9Ee*WwchR|d%Nk&iEoEf$Zsj+ zyNmQQUz{n5iI_)8Flm1py2{4|_P9B&qM=^2Yf$?j?_~vyZWd_XDoIUK3$t8#Ss9!c z3G-jBQ7dGmks(BWFK8{0ju6e#4~iG_c`FnGQ<&qt0s}V5xCAiGq={1q>SEa}49ooSBS8VvJ)KoXo(R!ef=e2)RH|1hMzuXI|i^ z7O*ePl(6Mdc7~hpo{S{9e%v$9Oy*wXVgcnD%{nn3t`QhiaZQ4vfX$rADln5Yv1iP> z>dLxG#4J};wKV~_6h>}Np%q}T=Ya+wa9V{~=i}|~jfi9bHU|3z2O`|%kG~M9n0#-^ zyLuX)binu4w*X`c0T0@?Vt~&>?_$+vv>9*!&J zuYL0IDq^JAv&;^W6APZ~FFTvF=I@$o}_E@4*dFbp-% zr>}Fpd5DA1n z510l4DG#-sXoiKt?$1hyE$4%l3r5R@q2++;jDL=4C|U)W6Gd!~pyK5H4g`fL%yNs} zZb?Nl0|d=FUPd!S391@hi~sMxPn&8^V(Hw%g_Bu2V28`GU%=BnB&e$PrUpO$*oXf% zYk;9^0lGmY7%8BxB5wgAP0Si(pr^n*Z-#4K{#2~jBGhXU>P2`{i%_pcsMiQ6v;jL` zQe0M6T#Pgr&~THs-ub>1(tK$tWcy+`ofj3+gasB4qB%h&!lb4}wiWdF4d2P>JM)Nh zm6hkJJ5TTJ?EL-r&g#9VkLKncEw0Ph-q7&#&vnJyGj^Xjv-_gu*Nqm-#*LPX8-GQ( zttu#}wpQoW(qz`wws^t!xw=~C(S&!^O&)mX{7z2anPX-pB+MFOn7hnic4%I5WVR6*w`0ejGplPqQ`E({jnJ%pN6vl2f$n`J!`p|uadj2J zp=NDVa7A5CPE1T)MJT-srFYKnuOCf9rK5o@02M@o3SvM70ic2aP=PkT(1KiTfTy|u9jJ!Jv9!T6 z=RtUWTsj2KMz((U&&iI3ju_CXpP*4G6e<<|R0?x4+YQ9GVb7;Q!FvIA@vjk*JrdNq zM9>h_fV7#lHB}a5Mgz7}BiJ1n6$Uo5*4Lm%Dl~(^F9^|>SATPPJo%u;L06Bbf;?#b zp~%MEqjEU<5S`j%Gi#2Y5A%y1e&QmHGKX)~S%a*n+RUCvV(+S2k5_r$y^<{w6xz_G zb3)H2!6Ef7v!=Qh#vZ7ZF%1CtQ^9j~6>rw~`03qsP$XlhU-?vOzc{qtP}C*?wTVOf z#i9MYpr(U$8e@eP_d?421zn?2LwYjwEd!?kBCwlv8MzHr*@yQWJ`)hHEy_OdDmCjG z_MPAnGjZwj&%dyAVpOdI`KEh$M=e_QZT5ld-_s_HQdc$ZtdZK~Nk@D9tusS5`o4t^N4(3si z6D^GfA3uM87Jq}^iLFnuQ=~g4sbN4%gB9vof$7Wwq*In^7Ls6(1P-&f3}XRt z?74Y#$j!^ScsA?&#VttxhXITic=sRx2#~YXp5U`#5F#@KEO}T4*;I!&c%4NE$Xke` zr@c?MWBa>PkacAV@DQ*f;`kEx_Ku~ZhxrUgq9e%OjlIVag!5?--pTX)MxRGHB4Hz~ zwP)Km+qYoP#<{~01r}n7FlJ;{R>H`HtSt9V;9tgVHi z7PCu3Q&U|n9qMZ98(Q0&8_v6+)9lo9aH$h~vDn+tY9s}q=g95_iyjz&r~n_oKtG>= zK)uczU{qc@%twL2!3d7R!QU?+Ff1Y@FyXIVOB^{gQ$&CoM}Zm#>(;GXe+4Bk6u8-l{;#Mg&d&j6M^1img;@io2S^Lkhg`jm8~(mN>GWsE1p& zi4*3gB8cv7NFQIypPP!AP>1Y*#d*vd#No^Git9Vg9#xeP`mhpLl~rL5tf8#tI&pYn z3IyW!cAI$MWIBt~R0Az_!01loNbMvw6JuP~hsg+B`$JYAidd zj~uEv<`@*=eyFtO*ru<3Nr9jIy^gzOZ(|Y`crShHkY}8r@~%$~X?1af5Alr}lyP(5 zQ@@t|f`R=l`^IzeoXPgy+NOYMv!({N*7vd0SxY8kv02A{hU3;M?ryM4uL_|`rAKH~ zZzYUUmAz5HdgXO(ud_&PKhX4eBAOSe;iYzGI=$5H1I-?G-7hY;IcdE}?=-Lqm@wV1 zLfj=efngnxS?yx2j93Z8uS`&(NLtoFwEk$cekxi&5UX?`^q~JyigB1q7%(W2S z8jxa;>8+sPHzUx>$Wb4j4lq^uXMXVZ52rIJnLuk>jh{z_6QUv;t%DLrO`3Mw%vp0$ z(bcH*zo)~}1pp=NPho#r+@Eqh$;w_u3KZ8WxFkS0K~lHs>f=VG7{LR_5eyzz1DpjQ zg&=#Ik5Ak+UrBrkjl|(-ud7F*XCd?pxujkwL}E6Kh++&y+hDDa-{O@r^J)EOUBk^+8te3J{1&L&KGE)|6F6-MDZ z+NW-vFrulUpqNjwb0RISKbI?dlmP(_+cYwiILnMy;^GlAQ_E1f2p| z`!xOCyj7#IYWVJcbCUf>Zr84v_8;Mvv&->_{d;>q&K^ZqJmT1e^Y5{aE*0ddsoB}E z&B`yXy5)^jwu-HCyaD&8TUK2(C&^UYiWM{Q`X*F!%{acpy~7$ImNv6zxi@7htZV-e z*+qkKMNrGT#2I!d6f<~o@+ak-``fpjHnBd`%7<~F*P|kdEB_MKz10K;IUxtg5>~9x_1Js-%0S?5_4eSBpPAN>=p`L6GI;8R;NKPtS<-D|1}nOX7uTD=uI1$kINK$w|546q7$?+{q8{VzPc^`+pwuJxJXcl3jeaNfxhA*vr+MO7Ujl*ePB_X*$k(^QWqbjA{qGm|g<#^AP%2Ji zl4CGI4mz4Dz@=I&F4G>ZZDX1^lU}azFa(4}1_mPLLx<>xT62Iu@hr|hqJvCHy#&!! zCUekeV`-I_u+@EA#COBOA>;|?Tc1z08L6&xB778cfb@)tiH>&`Rj`@35j zsx7!jT|;vlVlMp5vD%()Jh9SEA3l1*gjutOxlfpI&-@3MBm3xM%N8s|-lh5X-7{+z zJoY@?-9r;cr_H(Tw%Zb2IzxSQ?xojXFjSIVo5Z?N1)*RCDw-!V@C*-Ue! z*>_`ac8qnUZU>r?)bS})h`OF7*@05)sa|qFQ&;Une-HBHq#hUC(dE~CAj1dd!NY(1 z&1y9ohYuba07=l+ZEMLnc6|8oTP9pb642DB;+L(0^N{OcA+)DYEmy+dEN=GV_y`@l zjv}DAeowJ-UzR zW-l$R>rK6KxN!>tH7X2a-{7zjsWWCQSTHf<3Z;x3 zKb|NB_NDmT54qsb($StrkLETs_p8C^H)L$i%{q4UI%<(9gLI^kpo{=e#$_ETmOvyB zA{9Aw;2I80fEd`tnUy^kr=QxSX@sF)Ubzn(HgijIq$#XcH#aN%71EfSS$k8w_ub1m zp9iK)M13@EsR;>fZ3zjfZDuW466DJvZ8D~IjRW(q80`n;I@EK>fkS#=D0;8{Mx4#5 zj$x?dHF{jq(nC*=S7<Y8t)ne>LWMMVjYf1tKd!`N{_`TXVa-h&Av)dfKlpyws)Za5q^XI_m015`Q1L;#3>Jm z{a+^ae;N882K`DHuVIuD`yY{>)c<5$K`v=;7^Go*68rzkH)8)^?fK@4*$dL`01s`! zT8ISP3_j7k>yMt_?Bp+wyl7y2>nr;Em+`6g)H23fu*M^cHG{`DulMyd#7gH^oh!2^4 zKYIjNxq>{jvon-HbG4;V-xE8I$IcL7nS{eV)@aDUTDZ%@LzaR%Sj08oNOdE9^nmQ( z^oAidu1r>7*(Jjhr(n7~;E0kPie@R$TQ27o$;LD>S+(V;49|*msxfqwuJs?Q5$~a`J?N0KIR}U$=|PkrF9wq!*jSMUsYaXImX)*)P@F6Uwy1}E?t{^??`_^ za+R~53|8Q01#W~Nv=vaC%aYjVW)tboIr$oV)Gly*9gbUp0Sp9cv6o3YyWUGhl*2@q zk;?1%$bk~%ReD99>!#mT&a*&1@KGm55bpqYbS;H?wNq8MLp0i?eX`7wb=avJ zbV@MLi%Xm|j`>q2(zsuL=9#SKj?QBnoHQPwQU!=K9^y7`!km!!5%7~d7;P8!E-X%}f+uY+k1|xBxQMxjFSCQH!ie_@K&M<>l z(6nO&iwU|w-Ncz3d^ybBUNr4o{f$&N|Et=~AG87vm1!vxz4%Xc6A;*JcV6sBGb-2K z-h4!=non?X^`P3uW^c3hPRzfknfIMi)$C=usIDj%b&*+%T3N(@sv5$Zp%0awlhV42 z|5UT9z7R)B80wW0CTZIzgvr>XFiz)=-cx{EhA>LZGjDX`m9`!vD*#41g34^}upYFv z)YmpPPzK!w=H*G;H8yf>heRY6QdPS}Mn<|-zwKm(RRG|~I+K;P!E^ELqawY!s=K@* zM*%x#(4g2z<_(bB75QskN)k^7s!?AP4=q%*(YCkB^kPzHf?Xe+{Mj>>vv_n zy#n2qZbfQI(IkbV%rhY&F)<-w1Z~5ICyW?5GGV@FYkhIB zHE!rIH;vW}mU)@VO{<1>LalX0z>yn3h5{242gk(5MSy9Hh>MLGoH%OKpx6im{YJzN z8YS^N3bfsgJ=>;`&PVnyqHNnAt!-r68z@DdtV&P<+|}Pe0X=eFq;n-udTxXQ$uR+% zHQ+?a8j&kVnsKf!OS9uuWuwHh?NV7T9gOYeq7nrL;lAV1)8oVBunEiX58W>KCh9Wu zG=w8pXn3$rOY-Y(SMU_jJp)*{-)3hlQ2*=ihc8-+^0H4K{rT%}n4`1&Vp(l%UAx*7 zN;ChA(-*2cIyzjUZ@J}`K%QAE5TU&1D`Z^VWM+X8%N~8`{yBHdOd35lb=owUTXaF?XU|T6W*10n3Kl||&KyE;XLZ%G#zT5q8!GBlkmg&am z#xNy#f7nY7D-lG18G|Xo@xwY2HSqpQzYN^*54{g@#Bz|QyD?Ug?6}YIFn4x4-|~4& zKI0PSrNC)#HWT>Z=Yq#DXuc{e`+2$(;~`b(L#1=+9^|t$2u~G-ryB5Ca-*0^C`T=Y zrIsSBhw^^8y#MuEMFhWF9k)8(bOb}E@qr_ld-DUNFM?jfQ;_Gz#^ws_7Bf3*p22dk z|Au`(+&q=gKYPJ%Ya6Eor@mT{k2!X1%DmLn z&N^J%=`TLOQ|FXiwEO}^q`-eLc4rLO+!X?>Y>syJBX{^NsBu>ae0_O???Z`J}7TnRwL!9Xi$Z4?1+It};0Wxl~zM z>Zz-)3oO-C=B%x(AJiaKmrnopD^ECf&6W2i|NnO7Nq@NVf2=dnFZl@hTt&Z{5=&75 zCYA#75kh1ygiD*ltjP(gUf&Q>@GU4>6<)ZtHMD(GzF8C2p7PX$=8)xxwovhq_QB-o!{8ls`BSOwVx8k?5b6<&*&!D5yq6@E ziy`E|1)z{|)Ch6m@LEa-0v@8WfKcJK(Z*VzkkLx(4h8aHy>^hpuA=A5h}$S~niHuIUek#Gg-w{QC% zI?``WwU}{h64)smy1E)+ZU}$Iqwq)z#dGP%)(2mZo<4^eOchD*V$d?;=P5`T&vxC< zMCu5~6V3>ew8-Ij!f=W-CjLd#AdCD9W$ku$&U;!K)^g`#0o8Qm>}U{v4Q}` zkG-kq*rzFyu2lc&uCKoOb@wT_xMvl%SOch1Z-dcTo(ynvX>XOcvuTu+^0_>?JACXSmpWm3Y3rSb7g5mW2Z zQVqVXxQ=0kHt~Kzi0w8hyP8^LGA}PP0;9nUSHMO}W{%R*vi(Te2?5^(@fIs2VvF73 zlc)D`gQ0@{bRMC9_<`6qlhHQ!feI;0&t$aCWR#5R10*C;Q}%Us(NWxp`Bb3$VJ&K- z4g08|H?DV^4FUuBMZ+x;{K%TCl|W98bpy` z`}$8(;anu{+^7E{u&2eCj9(UnP_A`R7%sqqZVq;F_xInt`Il^$o73R-eb1uybMHe{ z3nx>>IDn3}&YlV$bu;VXhKPGCGNrsIu=&Yw=liz|1*S7I@U?cbqt1OhOGk30AB{;6 zTLms=4m{b|_a^s}jceCuHgjBfc(_N=cdt8PspKDK%vqTN%pC0LQGM<7%q^Iu=_qd2 zOK6GSzFss7oLJEyVEmD{h`F0Mm3oyNh(R)eN4r>6CK;L~JxXCCbA69O>q14ujkDo< zheAXC`t$({zxo%R z4Q&2fNHY6~tv1SMvI53is>^GSBjAQX5Y_v5`o&4-;?+lj9+qLOyc8y9BXOOPj0^A= z#R|NTi~*ii)LjtsAd~qM{gojspJD-kxRh6LHs@Tyxty~(Tfr&HIg^&7+@RnTV2JDy zWC->Zn15H7@6Dk0sfl{)$qio(8?Lg}oZ5};ToZX~F67>LgPg7($%>dGDd7_CAx`vUl&=6!`pLSFSx;cc?0C3LKljM&Z>6F^0|f;fEP4nX*o$ z`@;qayrb#{3IPBxQf+l>eM@yqeQW($IgGImlh#3b+QD$a%%~D%HhGWW4>tnf7Wo8{ zmp^13KcBFGP@m9%uz=xzT1J||r=!nq!x+8|6fqrrmKw&fNCJ;&X=|;ott^3`X9<$w zwY0%VQCkg!kU|TN4d7kifGq^)=I5L{cP_88q1{e2*pI1Jp|H0%ROI2~^XAB~xVW&D zg9kmW4bBERKu`AY_XT9(Wr@< zYK$g%G_h-JNzm9uM4BKVy-1z8|FzFxPu}2S`wxP&VZ|Ch|k48C7f*jHe4Rh>WeA-d^I&(1)ZPQ+i_QJTv6|N1%+MBnFM0*$e zhTFG_8Z0g@mWDfKgO||MP~Xtdh(am#mF4A)0G&d@h9j^MiWT$%R*-dls#Ssm1P-hM z1@NSSj2ADCwm2pUMJaZ+R0xs|zP40e{TtJNy^RFmR}}__&$sSX_bfeX-!5}^ONyZF z6D&hHO<8cC#CIss0;l*+;s~KhxGfwd#)k7KuA4+Bv8y z`s>4a4Gx`AEvK_XL*C)9w`}<(qe2}T9UUF2uE_XhOPZ=`a%gB!(1;|#Ek$KpjetWm zfY>)BMVebj%xyB}X2RS$g3>w)qMNG{GlT6w0v%mrUyIxyR~V)sMd6UmLHZYYWwXe6 zcnfcY70=Ehmt%uVu?lp9iAf0i=%Wv3j_Tv@_r_b(NA`vRqfMZ3&b#yG&W(x;3=Hf! z;F*^`dh3<3(`SG9;fGVAm}7Bc0I0)Hr6#eb(*PAFH%nQbTCGu5<(@iq>fAMDGvF%L zm*(X$A%Ry691l0uG|QVJ6~r2W>e^G`a-5?~O-)VtWNAEJAxSj_*&d4*ZP>73^-#n^ zF#b8Itm{j7_bdFIEi7h^0h$&>GME*$O^7elXmn-wOinF6&S=`JR-vqli><=h4ns4^ z)DYLM92PLHQTaXY@6AQTVRnw3&)!aKY2kr>@SPHL1kn}dHppzad?~w&{Ju_lihcg4 z_e=O00Q?LECzuLOFaY=&0BP<|7I1zLE~w@VDK~Tpy^gBT+{$K2*xi$xTudg_K{R2j zLEPP>u?9RL3|rrG;CM6-nKEufVn5g!di0QJ@DuwPiCrPz)P7z1PMqE+EQlqkP@~u^ zG&Cl=Ix0LoYHC^xATJNWjJlb}{=T8B6PdsUAp-E#)~qi$oM!^mAIlIqG5|PfJ!j#` zdCHY5SFTvOG}`ikA~j{@G8272`fmzlmC3GpMIPEj+cjfKHwA@jKtO(;o?`>JAY|Zw4r3~;209k#Jw0d7o;hQDLKpPr z?J{7(490W~<(OA_12rL3GI7h6I0W+0`GzJrZQ0gsTbC_ls2qSd+PR|En88ktuzWew za{yraG=KcRgYsTY1oZ5TX9j}(3>^5(8*wPRP3LCy8xwkC4XJw3 zhZ|v{P8M_~eZ6?>XuetI3+i!`nTwCI0SH8#jsSs~IA`H%u5xi&YHHeIdY)21=^Tfv zxk|Qn_3E{Zz6~mBT{84Cg^d{C?_pAqm%qRyy81IO9?HN6kMZNc3@|BzQf>}DY7tWf z5C`0EP~>PGV1ZyAcp(B;q65H-4hjas9RTR$_|G4cph=dP4&a}C!9T5*7#mJ-#6J}p zI0&#*0mKy0B-{kNdYMyr_}gzkKkeD+&mbfoKjITJ>1`%7rea>AyxOF+$v|j!7K7#i zxQH7z41!H%CITtmf>&u4Vg-3>rF`w$H7jBf@iEsjlL?wsL5?karAb+-18enUnrs;T zk%O9>lL7`{k!j$10O?~!(qn1#AsBruMn4*(4*_=x5kzG4z=lbJ6Mtkr=2Fv{KnZ%d z1rSrn8i?iAE$|q`s-i#|!~+%_;uyi4e8Zw)Y#cs(M0XE^+sm{1bqPc~;k|m5v17k! zGn`c(F!qWycWdfwk`3ODj-5dYot^7yZk_=vIfo`cU?sAJiy7sk(fN^L;ld?rm&HP9 ze97{TO-ky9AAa~@_s03W*6eJ{3$kVFR?s&#HLDsG$%ZC$4+c5p-fdJl8^JuVYUJ3| zf(D3hU{ecM11%oVH@F3YT2TrQWPlk%K@coRMnO0qcWsolc7uyLq&W3oF61+pp4b!NCXoxF2uEapRp zGqVHQMMXsg3g#P!8AYp!3pqp5=1T1oj{EQP#H`wS(y^4rodDSTepCo88c@Fbm`KgTU2;> zcr3P$DMG6J&BR^-Oi-9K3YEeEAD4MRTy~6&jSXXhxE6&Pu;4VyKmGKF4XdF+&3(#|YqhFpZ5@I?t`h1H3P<=o6oLPA0)=zTkM_!j>(!jvP7u$6@L)dgAhL z2QVL|G9N#H`cVZYK{j7abAX36Cz)4lQqy4JR&|sf@=>{ARACrZ2u6hl_Ci}!DA0mV zX;VT6>P~`zno;x#6Q;NZFeMm%1o7ZOlf4d=Ie*%`*Bm8d2ucI=&RViwGD}EUDQb05m z^Z?EWlNc6{)?}}N?M`Ql=v*6>jg3Y_;?KfJlFP+1h+T{1IiuPmo`U47cgAcWGXxtR zmcauw7+m$K;Bw-`7ovsDs$QL6pQY_R2%6PCsPrfcybtVqE=1RKaml)PkjYfb5@JDk zL!d@uf^j7<#Re=JNv+x7ocK0bD!7RakH_GV8LdxAtB4{8;AuxZ>4V4t9RwD*Eo2tN zAdLqsH%&}<4?P!U(S2g!6BOqr#G)wVkHU9M-gvCB5q+VOg+?|6fpg(kSmbzG#+rfG=xhL_ks+8g|78Uj(^e<2>$`V z|3i8L#VBLdX*gM8VRO2aB@0ew^XSp&U$3-x@lYrN1H5z^NJfQ8kNOLP;=$*!55cv$ zg$Yhqu58(VkOXnIBqY}aO%Y3;^b*g)^PH~GUD`UPUhq5 zit!}?hGPK3uE1zlVAKb!20R@rlI{!Xj@S+XmLjoPD%Z8a)K&MdT9|Tn`wmePko~hx zKy5|C`0E?uHsa^vkwm;V0!_un%vUAl*d!d>a% z-(Vx}DmHBTbYUu0+<-!?`0Cp=3l~!BwU=a{u3r!fq4f@JA5y>iia(fbd5QTnHj*&c zXc`;sVP183^r4#hfWLs6Fax^3PItKGScG;!v!Trr{x#mek~1H$8>rD>H*{}9cf@)K z%M|$k^bbi?xd;$l4T!D=L>B>~ivZCm{2UIFo)RQ+4cG#nu*c&WevqowIxd2?^cfEL zTWSCwS6Nw11KTkjf|zO#6yTtJH?a*dm2(e<7w>QWh{)t76cfdPY!{J54H+_|i@&bC zJSryAh<*~42?=&+tX`vbY1eyHZ0Dek{RR&n+`-PReGF9lK~bGM+G7{orp?5Ole&ic z;_X#xwXXdi4MtEyUDp!<0X z=3CJGF5l2KVN9?-ZTjNpbLUp9+PZr`nA_Iv=h6Y3tH)1nSh3@){onnFWW~LI9Ne=7 zo`rSGHvaN6nvbtqwPwxQjeCB^+fS*_9RKhBeZQaD&y4N0i1vA~m3MxyiFYv9QXfl` z7Mpm82HhhY&lX@HOHjB$cl2_puE&^w033ufyd>cc^aBcl8$-eufBOEqklsU|;m_x& z!6tB(o69sZD~2qohzX&?ib2F5aVE?j-n$Ylx0t9lE;In(mPL_v#PudpoIrxO22p|# z;VHp0{J_(+l|dm`JjLHbpW&B|YEUh%oSgpOjzpsGB+&OV(Dx3|H`pI;kI}akZW4V< z6mO+(;_6Vx?^8Tqk$COaEg8V}ucVD?s@Te#K*3MjkamN0a&khboo@1!k?~y+p#R1j zos0lXaL>dSK6rb|h;fs#;~Ls&>Rd#D&zU=WY629W*#1*rkAp3p&UNb7pBs*k`3z_6 z{*9`Yl(&kul0?;wu(>LpzP+XG*+-Unilnqpe?)Nh9LqcU)YTim{r20`v>$gLIdWwG z_HSZfHu*riVDV?mSHmo__Qxgt!Q}81&JQ6YKU6Q;44rf1rX^JW_9g2%Pqk<*zU7-W z|3>GIf<)&^iOx+TiMTqGLWOO!t4V z6mXT3Bg2dTN>d1kL5>~N4feGC_alT2ntednxU$kUNmSY%;MV#KYK#z(;}scR{D1NL zT>kY2HP3~7UIh8v8`Rue5Pe|Pv8Fm;d6E@bTDAO6A@k0@iQO>g46uVr22t>FdJ2?G zIvg!lj)cy#75Ba_XaE1=!7mWXG4EiESG+_q`X##e~oi_`y7Q#IzrM zl+XiBzxoZEJUbdij^2B7@=LH1C(|j;NPsI2Jj}89QPv9ke{GbdGUGDYO4eA$0@ ztV$r426i=&Mu3HxeBkCk*^6#rmXfV3s(@*2^fUFEJTV7SKTf1Y&jxCGPzXczX zr6scj$lmusrF4EItYflTmCEE8@F>I8mDW9BJ0*ohvYmQoBke?zgE_6{Q*cyz3Is(; zDTy;AY$G|7z|9#Y);%<0ZwF^=&xuCB&Oi}X7AWl}9sl*C65pZ>q=~@v6kxg?Fpb(7 zh=9beVDm$0mLnVrJ4n$9Nx2m3C>g@Ac}K-8JWUabl=TiB8Vs6{>mM63AT|uHj)=}Z zVn>b}H*R>pNN+p8p6?Cw=+hNb4(pK^&oK>L!^S^{Y2wva4_&Qvx0RtSiPFgpU738k z$9L8b`_F&=)7R+^-`SmO(grINv{^&8ZxcTp{&z1}UE17MzR>Qf9Vm*X}IhKzc z7N*VLyk+y|%^Q|3Lq+|x6`PsnY+a zpJPr)@um1n$w-Ecdrn@S2%{Jx9d%rvK(zwc{LE<4K{f6nWeDbEByo0r+WREB%dFnuGQ_X+p{hAvM5F*uAB$|NN3Lm8LX*6@8lvEhjalq={!a9!bKQPKGtncWlAK>tB6WD9SxM!Z7K7IQ0&rbah zx<3yaJ#1)HuZgd|I-y5YXJnSyc3~YG;XS|p=YazUvPv7x=5h-*hfTF*ckfof&@I^5 z7v8OHI&|>VHDf!}>ONtT{SAW zn>4iHRW(-D+qpQZ+qaMGl70iGuNsxc*q(KSNf%&4Tqhj_HewL160!9;{wkNEvnD3r6pu)0)M7CLfqeiD*(G_tV=?=gOgdL zE+7`#E-`IfEd2ldV-g}Gd!g5~?S~^{x;vupb6I#?{A)2nnvkZ9jSUZX^k+#3`LoZh zPj=JV*|mx4+o!W@T}vVCD(ADq!x765XODLM@n{^&H_MEzjv@#cg1H<{8fRy;^TGpeM~7CR8!P0{jioqxWt!RN=W8>)2wJo(;$Npn|3hP;l+^+HT z8I#yWm11}4(wASB+$`O(f5-a0rI+whsiw{aEzRpwc-SKGZxA4iPxvy@ix=>?MPk<= z8yKNlpXOuI7^P@w6~?47qN=SCycSMuIc@D(nyC1Xs1U@{tKq4@bJ!rWG(k>#UY5o` zPx&ORHM(suC)5pwn;S5bBPAyrRu+@oxv_3|5(2j6N58rDO_E?V$zjOBOEQxj=`;>> zA;}3eoj?^^siKwv-(>)N?6ETdrAZD3V2KGWD>U)1gc#obk5_1D4ER}+e99ChY;=FE z5D;WE-tw|~<)Yoc|N85K1ya7``t?kRT44`m6$qAW{UBc^$XD1yfdt|tvp&`kJtvKX z>__C^4>jJmA4#@3DVj{w@=?*A4|=ZE)Qa(*K5aB+WR#Rv6yHN+20W-8?W!_hJxmp} z$zltvo%bkwyA2Yz-QDx^1Ko8xZ&w2haR~FiE2`XKqk)YKp_H(N>bYH%Y5B0R`aqs= z=>Z^EUs^tHQg&fdYJHMpgRF4QfS?Dk*=mK;W#`iJC4Pi0Cpe5{iB3CmBDn4J>Gky^ zM>1Kq;ITyb3ZYr*-w~C!XU})vQKs~VOqMBlEzxCX(PE*G!^Qx%Bref8pocU!dqCrs zp5njW1HV2MP@4^?^#J~QKsWOu76;oR-I2hjJE+0%C*hG7$(CGjPtuT40g--#^GkXj zG2rlqFiHi#|5=C`@9w8XX;-;_6~BxcLVJg@m>55FXJichWcOEKrK!K`GZY`@qgE zTd;g-e+Y;7xR32E=vi~+Cr+$fiNHU1_Qe-pY*@Kw5nH@?)#@b+w(kD^`|o#s`}Nmf z$KcH$DNH6)ax$e9!C_usN;%mnu8s8-$S%KIQhK*Azc4fN^2N+*nY;-umB!}k%xeYA zi-bcKgfaLK_7H?CN&M3DNdOm8C7mE~Y12Sf0MaoLlx{{%C~SbR*#{4DutOn8P&3UO zl?(LxfBD>fY;^;z^Z~6{u_bu&dJ4qjtqujk231)k`;xmP*?~BSeCPns#0Z~4i#gT3 z&CuZq-8;4i7COdu89(afS6`plwMXyq<7-ph;~gBxQlzzW^LEh}Wr{9=0|yQqDiqyK z)on~#oVIAo*RVBS8R4C zc}3C>Swa5ga}r;O1BFuu?9qT(94I^v3o@95KONj~he2>%33@C*$5$QJ4X8SYB@@j< zFk(-E99VX{V|HBd`(g<>qz@H;4rge`kx$h$@q7UA#O!|4h%YxJI+! zIla{;W=qi3}j$g>bBkd z+y)Qr(?3adBogT<(SrK71;>9&PTsR5NrlNY3fY1h!M=H*Y7&kN5?77bUIamN8A*@% z?0ex?srth^n0+tMU@zjb0x?zWR5@Mopf-94JrZN_0(pYJLRj$yLu;8^>!EVw<|es9 zwGrfs7KEWAsTAv(mNsIH;d@3LL(IUTLx&DY9MUgpKz#h5zFk9u;t~_%+JyR}1aqD5 zOIS8Gw)U>}&Ou$rzVy;dtOJ5&8(OFjm7TlpPGKcf^<8^M4(ipbmrKjNTL;bN0|!Bd zCu->zE^KK;l)|!4Hh=cn=H<&$$cFGXw;;^1d?;JE4ZZH);o*d{)oZu?7exIxSj5{p zSiaV5Si5rd+8y7oi^bj-1~8V?9;Jfql&BL|c{{IJrxT4vv0PCL6yCMd1sKid$Z!PM z1lKm>^YF}^(p zMkl`i!3UEf5~Cy01*>al_wKP{fV*+AZr~t@-5J`4DIq@GY;2Q*?`0e~x$o@Rvv*Za z=&p;DB0G1iRa8!Oc5Bz8M-LtBY(iZFEU)^l8#Zm!NE2~P1sC(`;JQB*PMCMD#0@9=sKtLZ|5;93q0pIl{{T(2nKLCbK9DoqN zG+TTj1HgPJt^+z08H{iVIb}~;4W9a+J}+UDVr3eD%{*Y!8Y|=Hi`+RViKyp>e`>Wt zj;69G46Y=F2UMi?g(`a!1=dNz!kUDHK)9Z<5;16Eu(#3(ZJ$t+BB5UltV5=*Gluu^ z_kV6$;_zYp!d`!UOjM}7{pgV~QN68{?unI5>R!NH8S#b&h2L_!B5efihm5e^S0Ck4g4 zIyrgbkv9{Dfj-Et{TjA;?{RxM*%jX*ub*Xh$YRD8FI$Dk%~h+GEm^#J+0wNeHgDUu z<@1dj)~rIP*rHGPYb!okvScp*o)@uQBA7)ub`&q6lZgK#`14b&pPP)&qSB!yku8As4&6~`Nv04G zal4@4*Dd@%b~$k4!V4~AzSxK4p^7{yFEF^}Q@8^$AgpFmCUVk9Vt#@ec)k!h&ZrJg z+6qiQNXSGC0TdW4Lb3=r-hU&P3>5t`2K3k-^k|KKLB@<47CZQqmBbYw5%d!IAwmaS zg(z;B&^Eq*m%7~BAz{Wk#3q_meqlw0_1y;FDX71$^0;&{Bp{$|+nzx!l|8Y)H75rI zXJ+5^4Cr$He6DL?R^}bB-R9^5^&8?LT&S@3w7w_wN6G?{E9rw|WT{h>XITrs*$3acP( z`6)0zSIo~A^HYG=VPlHbA``Q+O$f~9TZ=CvqlKTuHbJP;0Y)jP&+gv6nF@nFn!L0u4zC>~~?EUxS)f&L6%xbyQ4u9oSGr zM91D;JN6m##)o*Gxm>z*>F@JM;wg0v4Q(F`%TaJUurMYpiRH0*AJ{G5y8UY~Kx`3q zZ2tnCmghlk%@UT-H^XOz-Ns@UurAKbiIWi!z|5dL;vM1|7qBI8E2v1M9Fcwx7G>|i zUwfd3v}R&j^$po)6}apd<84xq1C2bj^$` zfE?RAlP$PE-U+R{0g4AYMkFLW`z*9rVUMuKcH#>tHul;p(J zb8uBvFi3FKs#zAK)mb8O()|ESSH!qTXAhK!90JZbm2=2IbeJB9$3yX$!cX4&tObjtFJP{ris|J$h*WPe1r850E=n!u3g=8%<_MD2dxSuDMwxWM#?_JOw315C8M)Tp z+11-V0QL-HG}_%%mjndZ${H&Kv>{Pys7jSqV^V7zap8y@KF6vm$lubcdq4w-p}y)) z6|l<;=Ohcq^2h6$dDWaZKizroN`2WW&$rl&`TvDu{)fWgGqfN5X zMnJ-%ELn*txfThN$AQ27`+>=y_8$UP_cQUt-DLZ_C(fNaad79(od-{xIwc{})drtw z0U|>@^gt>hk^>nqhHFvqOIGBq$ZR zJ&#bTPNLL5FXf*`GX$3>srAPnj~;!5VwDobQg^K1QB`bK67~FYd6IaYonaEt+dm*+ z8kEfDAtFYvhN0a^OI>0AfSl{=gMuC*X`MvU_Ck%LU~h7HlB{cLn$dCLA=1`Kq+MEc zqY7=Vc~18g40KZgy`fbAnR z@C0ZcCkHEN_`o>bhs?u7VnamYq^Nfh(6#1CLg8?E zm{@FxSmc&UNLYf(;{s zmR`K@WUr@EjK$*oB*o|+P|SenSYaCy1h+j*FF`@|PotMEorcE>!=Fko&CNAWqnG^b zx*`O}NTqikVTVoB=t|=JL@{JT?P8@ET2Bvi3r;b=pZWeEDnCDsVhA!%a*gy0F$gxD zgQXNpfO&#mB)~jLFKUTiri_mjeignDetno)2rf@hOUDj_`w4@eN-YGHC#a>aO49^3 z1eiQQEe~Ka-Y8|8&l2RqSfft5r%+I#r3+$#frENep2 zCd$M$;i#+g;t~@A(<(1FtY81>r|S{nhf@ec?QvJ%&Ye5^y6SYUrKta2T6wotu#{ih zyIYWJ>IF+fU9){yWWmjBp}-XKBwMnvG5Eu>(Z<%c4vn489ox5W--!zsifowOGdLzq znKI?M8G{Br^D?zK!D;j2`!k=NK5NRjfdfsdsBtgMdFRcy5`+{~QCcWm5H1R9lTaAY z6jDL0;F_w-VY@(V1&L)xM~^m8NJCs)22P$ldHe`OP>!E8DZ-I?A$9vcWUTPH4;W8` zT*m{o=DR!&ZBDh#ksi| zC?ubeb^dZ@Zeh{giVBl`&E3LV*Dqc8>n!rIPop$f2*=1HBu@Ks>Q;{3IkL>?=H=n( z>Fz2?kyZ@9@F<4)*Koao=J670r6frqNcTUA;dA%kJh*o* zBjeoZJ$v??K6l~5zhn6E)$)%RR(&uFSeW?%Z*}eX>g>1PdTTDC6#kb8=7JiMx}>}s zMt9l2BADa170Zvn#Z#;o@n=Ia8YMd(Lom=PiF!!-#}J$gOBZrVGBPe)!^O1=w{QPD zf*)V6|A=6TulZsP2ZOH#0>kcD!yZP|vVyY04?YvMO0c!1iK(uhx|&&ZVY7JUij)8j zV@i_Tf6$%7R2E_}_6s7CasM#Kz@9VMHX;8vAG-ba+s>VW(y=UV=A6;7!cO5V5}iXV zG3d#-&-Rp4rvkdkWB~z!N{QdW21;4fPzUuD?@JUQ}?`>=kh3uk%Oz2IlSerCYy#gT_sp zHa;dM48C8*J+#SeP^*3W(Qn9PN(@iQe}lgJ{ssRbnU`Cg+lys?l|{FAcX0J|L_qwW zN{vU`c7Z|NqI$=Gfu)KAqy5r_&LwoSvvc->;Gsi@4<9;q>?lqe6^8NXNkHU-gB~c#{c^8d z$(7mJ$*%qN*R=+PqM^2?wzj&WqMBNB)ipL|`{kCX^?G$lZtK01!QI=<+1Q~&NV}l6 z?fnD^+hjK|fLaa0eY=66EUQNQpE8qt-@XF}7(55!Ujj;gL#Doshy+qb(G^k*g#~^Y zc=jhIRPk_kXG((&tV0HqytEW5v(m{9F&d~I)q+9}R*C;~%GysfWufAg(V`s@)hYVt zW~D6pj1uERR0Zx^sqqNC#oAr^BwZU#*8L>e@yM^_XO!%CU5u2kWTiy)g-hqpUA}tp zg8KU9OIPWpw(oKAYrjGHzp7gag}hK!g6QGa1HD}3`ZE1A_1Wf#Lb|ET?p9Qm6qS~# zTM_4gMmUdRN-wLclLd6>5Kv!QTJI4U=%N4V;PsrG>j!^oy_aB5V|6wvz$K)y`p{U( zE=S)7A6P*__VtSw)z`0IxpeX3rK_-x!zU#=eh~v_&lmFCeZB*+!pXd&P(0JazQ;2t zw4wa%H@CihbJTg+85!AyrKM`hn@2>HMbH_@$)hv`BD504A`c!seC7h`;9WR#_@E@2 zTI(%QgiUL`FilZKMM*(!Zc!1_96I#NfdjvQN^#k<=lK2CuaD6%4g9evh9QcPOg{0* zkv6etAW|h%i4!I;G$6`zch4&*xQST4Lx+COIDh`sDW-w#+hFi=cMo8i8w$n#^hJyI z9%o2uZA7t%d-p2-Z7fAaxw!=;6%|Z#;I{(@P8>RP;sC~?8FT&m{_#C~GL0o2Zc~)3 zKx?*%6DGvcK!oM8Hb;)&SN-w31qEJ+U_5*3)cNxnKOZ{8G@cHgo^EJ)iv-^r$M-H; zl#cSSOw%AVG}PU@SJxnog;q!autf1J9^mKRMo|TWFmVi}t-XV-R;4_A7y~+}MGTJ> zd7{mY@_Spio+@`aeAuSxUTML(ojcDJlnT0uee7#%6B5Quoc8+bvBFWDJB9a{{^rdR zxB@(lMx!U}`^7hrPe{Lj*bh}rRYeUJw93Q7+sDt_3*?CxIDz1m=Gxk_Ms;0XLv3AM zZG%Kr?ig=t{7pNIw=d6)qs=2cD~K=B)+iJ;*%{e6wavBK%SV6PwfDF4M}InXj)HXX zi_)9q8UFqmlvXww(Hr}Y{eDavtGKx4yNh8Zx67({S}49rPV3~o7hjBp^=yr>M*R5g z7)0hSw9z`b`5B!2+_mnuC}V~IV6a#C=R{phkFN2<2FLV|>DRX(iUPZNx}gpPid4A> zUPxDkonCM6=z?B}dNgpN4tTI&!$9Z6yh=;&6yB{U$ELl!;%?!c((3BcyTypNDZX1; zEn&%sSsStLji6p5_=HhFe3*z1KsG2np+dI`VAgKmK9D_-b>P6E!$%G=<@f43N2e^4 zGV8*jT|2&FsJU6}0lfz1GPwrPIm%~j3_90flQMY3?Ag;MGSvRQSAZfu(C!o^H*=Yt z%+gR*bho(pZh2)@eG9WIEfrL-R&coiE@`K4s;n%ps;@^%10p?`T}unv`4to(uDq_g ztklXCscLOUjGQXSd^j{uwWlZTF;Nk4e#d?C|r zq2u#Sa3z0+a4a*-r!7b;Mm+*89LML+pZhBv-2@(eh3g6b$8opx=VwAo9L`NG|82>p z^Z$c!6W49c9kqITXi?z+Cso6-NF~8@)OGh7nxSkr2+GQe7WZg$rTqoPv_3;=@+HD2 zjH+Puvc#)aSE|qd{>^gAJ9!7z`mewK<0j{eQLF0v&u|zAV60>v?FZVRjOHNF23kTE=At`Q zZXqY8jN537TYyDCRBLHU6YA2Ob92+LBKGI%sjNFlL0^ePu&@4NNe;*j%&QXo{r&Uu zg!0PbYI$lR3V^KzZET?Ri*p06*9cpMQ#cP`!hiDflmHO{R_W>Cq{FI&Ew9;Dt}>^h zN9Oe`)OX9ee(f6eNd+jJRGfj40F*7FG8x;IWI>w-M}??JwpEA@4sLD%;aKIaPI!}z zjSaSBsR{=LoKjMUFVZ4=VYXqIZF|fXPIJNeat4g6im>Y^;pefv??j;U+T{fmT9)LPiR8CEE1v0xUF*0Z(UIXj!cy>7-_Q^ro!}JOH+H~)0nz<* z3N3T)O94dX#i0T4Db=I=Zb;(9(agE7PS;eJRV?V8X`F$(O_n(+e2SV zOiY~k?2EJKygd75dT&it&gH9ui<^eP;NVo-Tqkic1t36nb_F0{_0mDX42QA2EpAHO zj^Y+!`An)AC%ZH`OTWx|D@{ZNpW5G;_FByDz zjS3pBL0HSgiaLf$M^<|#dFxs~bKhIX1tb-om7^H8O#YEmq?5e3xp=UAMdq$dxlv}! zmQ$fwIex?}r!uqrio?{1?~6Aogi%IGK80CpSwoCMI&tpiG9jJp^Ye2NG@VPG zqsRfp^DQu3M2}$TGMSw%gugUNnx?IRmqkV;SaLtvQjj5kFYo2aAw^tZ4j{D!kg{?C z7eq9}=ZM%41-=UUENy{gazoY)31W!BZo2`9dx1K#BLjs(dst1o6+B>30RCHAIz!?-PlB{_y$_KLBCu$nHy6E~3Xs67B zV3|T`M7WbHTbvka}zB5=(d9M2Vpndn%{#-r}OiIa81bM=MhmO z`injJ=_hIs1$3UD<>L4^V_|H=sd{644AhGlRExMRVGn`CXqcRnm78B!m}9U(hgr48 z#zw2P)9X==QLP4v8qm>?T!Ma1GL=Si1 zk_uFiF+{ci@tUpZb$iK|VY{1Ry;``7(riIzcOeBet=z8F?}&5lC@z*nlo_ zjqq>NqyO*`alJx4?4En|g~S1g6UL67ipm5Jy%h4$OHZn6GmG#2|#6HqPfD%!oPBPq67h~w& zv}?qb${h%#wTCPQXD2LRJV1OTJ^bg-Nc=MlnCvPj;NOu$Hp_5>T)ujh_XpsSnpdu0 zyMh?AYp8D_gPV`g?^i?`+S$vzy5k=g%YTyr}Tf zrP3mkvasmVrJ_>iTvU{u4OsAK(Bi^7#f8PicQ7t9Jc00nn*ki_J#+T(;LQhkps1&t zyQdq>v`>Gl1P3p`fpSp10S6f6aYLU39(4--mzRgc2$K!CG=dMnu+jF!d+b>6;NYIF zLxzm)iI)rp-`rANuCFut-XBX#J9gyA_RR|ye!ZRP%;vyAZAYK-j@oh`^l>1{M+nPJ zWJ%&<2q}Rd65JGRSsd)_5cQ$Ax5L7G^rZ)qj%;@%xmzV&N{WDhnleZ^S0-d~xyaWi zbzSi{Duf8EPir?5Z0JO_466<_WESiaCnGv!a$;hCh?D+_%+AwGR&n#U-%5&WDhkWW zDA+8@(gj)-on3gyLqDL1&VH6CX4k$$`#^6G4;N>@z~IP8q)H&SnMeXONB7c*eK88E z*)s&A@Wq%B(Mb&#sPr(!UeO!vu7b^@nLYKhzjouM9n9Z|lx=81Xa)j41oe+?+lEIc zjz>%$c%l5`X!DO0-?32hLM&By@`!yQT3EeH%b$Pzu_cy=#!k^}8Q&MC+`hyX zH}Vr@!_+0qmM&kEmV%Nh;V4@&O}1!naV#d++47QV$+w6$`wpd7U$S&&Hq)k!#a0nE z#dl*Ro^1PWaP*`}lcIyM!>@He#B1 z+ho6>4}rL#kdzhWmBAFE(!x*ybA|NSz>ha@<7Rs;t_(5i<(F`~$F7TP+s$9J|?B6lP8#TMUz+ zqz4LYqAUB6qXzd&7#Y~MORzB@u4e~kuXOgRs6*yLnU{;wE5w7@FIcx=`KFcMZU1J^ zS9?DHd?T~R&}A?PSIcmbQ<%f-vpMDw^~(e1au{VAAmf0C`Qi%sl0*fF>g7Rmj*q`C zt=mA1{x#s^4d5dX)EQ{487@;n@K6^Fxl*Q*J5XFPLg|QPDGvMAEeF1)v8wrrIU(dH zvUA6f_5lF_Z9*bD->cX7hV(`#b?*?b`pM5u9Gi&BDZ|H3d^Vs>4` zjE!%@Iyr)4d%Cz#ij19Jc^7JfMc)<~GVS;a?hn6U)(7K-woITJIPY6NRHdzj!DDqA zb+~*#2=TBB)6$ZYlhe``uS`u%TeT53kxi@8Qa}4_-Ktfq79(A0)yj>mQ{D~ib8qC8 zLRy#R<(FWOU6_}H7{(m_Le|4C6y#;$g)9aTATE)^9FcGo5$C|g5I8J90epn^B95Be zk63yXS`Am^6Yt^ckam0&#pwyC-_PD4tD{VDEA#rzY|8rrQ&7F^>+RC%BUE%wL0ZO* z8zwO*h;*1C^5o>BTd58E%a-Pya za0NvVJyBi4N4)X=8#Aj^ZMt*`_4n;CbY@jyX1)z`iH(i)gfduFd>7y^xl@5HgL|h~ zChUqwh_SoHA3?|C#9d!gPoJ@tSGBwD%CdKVBeUz+!@^wB(+f1A-MaPd8iZ2fft`DI z>(*A0pUzzJb3l|-P(K%{@126%`FDy)UvQyu(fFhTS5T|af)zb(DRB_C8Yif~?ns7n zC*STrzF&fh*7(4qrUmcP8cFFz3>`|kf))bXqZQSA4x?(^VQ z+rR@nqzLigDM(kvA^jTJj0S*427^WdfP(-*L?ko53mlQ0T=Dp$s#2*^D(Jwy5?Vgf~Ovrm|&YUb;6UmX^1 zTa0#TSG-34U(Gw<(Nl54$?2s&TY!}5;vTt_ZY`KF?-iCCiFV?O-l<@^2U3DMY>W9HNV#kE-Y~!VY1F`ezS^?>&D0-kIrY0|RT#=Avro@DWVx z)G5YXY%W%hXt1BwFk+gH>2&GoI+d!h6uNpfL!GSb?8?%zO7PKAMl~_0qB7@rOy}q4 z;Nb7$>+j?5@AKGe(rBq7p)E$+THhbEEhNQS-ZtMcPl8ygNlyhrKDOAWl#ACtHkz>`&+gDI~%1 zXTHN^e2QY`J|^Q6jG+lK8K0e4mCt0>xna-BWHd1eZvXa;h;+rq=kafZs{+r`H$u=6 zwD>ndYp{MJRr#Te&Y>8iwaSm+^*2h*qvm8LboFZ)C}Voz!nyRnFp59Z&t1T`VJq)o zft`ihePl?Hy4X>S5OqR6Y#*EXd05C7bz+44?%gwA#>3D~k{%F2nu90>t zsP%dQkLT%j|4^U_g0c~+DfodUc@`B3T3@g&qqYj3k17pa`)ev7$19juIeG=4N@zaR z`8x&qdAG{SDyyoI$yZZVS%!&WW9dMCX2?(y9^`eEizZm@%qd=eMo)LNE-+ABkgLI2 zkNALqHt5hE7~t>w?;quypQ=kF0?vtmb8B@8uL~E?rT;0x^lMA)KwcXW7o z_~>_I;5s_Oyj&YNl*0k#@UH3UUGJU;r2pcORx!D^*T}cykpguF;V5T>)v~uodXby@ zV^QBi{J+%F7UKCu8Puvcs2_tZZOx_3R%_(IE(fs7i%hmf8hPeO%pqkVADJ`luxNub-7x z3srEWJ|CptS}QoBb2p>_$xrYD5HcNB%L+la0S$Q>Ow4GH+u#^Gut0^TMK z?-Lu%l9wyoPvpE-TzkH7x<>&(ex$B&&ngR39D-D+Yf=toUuUEbmOT`2Nmi>K0-qJY}s zwV!^rk#(dP9WNg{15_O^A3vk7SKIIo?ZYF20|j+(psz775aJfmZs=F2k`esH6p!p2 zX$yc&0B$)nQ?7%+QiOVZR!Evql+VKfm5}&haHLMY$i(NR_)sY6pFbpF zi}uQHSRoX|fd@Hj9vVjw7=TE=99UB)6}MLx5m&se7ZqS^bX`_bdxO#JGP z1W@Gw`0PHktg*bv)O}S~>@uD0t}u1}ZKS)x5y8}Q0eQZNcVKFR3ldr3j(F%fX&mJJ zpc)d7MAKwuL(jU5R1y9+J6iZg-qIa)X9YWxY{w2g42sd_Z1A(ssqs;fNv2X2F=c)> zG@TMuc%wh1!2w#0m-P|n3A7)kyd~<6^-$gtoN*|1E;Er9$CUHt*&gaOi=ow{Os}8C zVkoslypYdJFf$>`Br7V)V#r>CGZ8!;)xpHdIq>2cscFeH_3y7q6Z?W)g+rW3#n2uFx>gcfB0ebr+jW%Sw&2qol{sq>A@Dc zgQJ@(zv6SE$|2OVmUhIbam9Pq+EAyhn^&&h$hZdZrC^UD7g@YeAv&NW(<0%FV&CPs z*g*O{bX=0_^_}(Tyr@_TYz88;9r3M<`%am5JDqOU?b@dozV2Twkr`w(Cy5?Ha) zJ?iBHMA(9hG7a3bwC}oc4NL_R0Lo&EpmY_SI`={sD0DAXJGn>p>)L;UE7PM_6JEY{ z{ra~scrQpxV}ktj*<+s##$I`}#T)0F;O4Y*o<~C$PBT5?_Fz@^@7Xg3T1X7V-@q?T zTdCW3Fa)q;=*)7ByBk`oA;iP|!E+p@RBa^!BPZ*lwb}~u)xa8HH$c7`RB*^MnEOmB zGy!Ucx*&*CUnU#!W^&djV#dyV_Eny>C3CXx$MnrD^_7)rX$vs;Z?9j!){Ha}`ZCyg zK%(M6IDyB&oEM1N!{iV~=}7VXMg;B{yn15rJ^hs$=++P~u+*7Qn;GT{QP4nthSY$0 z?*3DJoUjwV4MhAnhjr9o$ z=k4+Parpgc{66imxtF};Hnv^|))Y2ESGjclR&jY~h>kgzl~vc=xqVgeGc?yjg;VH_ z`ZAV?2BN1xopi23gQhlOsMwjGHgFvo#2`E#BMuaiK%mtIb_(|MFti&!_BDj~&VxNc z79OZY)3P4Wz<9YZF>U8ibB?*ue4U?p=2CNmMP<=~-s~(rnDdn@$A9@Rdb9rZ%NLv1 zuV24q+vUtF%z^#<>#x85eP!+EyLRn5y?O0Alp}Z*Zu-~BJzuBv2yhjuN?XbitKC>b z?s?}!*|3Rt_P(?SGV8>#pMUuF=O2He_v-A8p&`QE;`(O9zE_lyf1cxr_6W42p*?~g zFrqzzj?zStFfQ3omx=9q#2}nwiTYf2LigdrVfadt2L^&EDzXBzvJfG+YsdVh$A4P? z#q#B!f6kWaa&w`7kRgLirpt7c?2de5$y&Nh!VF?65pAUqVnEY=_*Fjv{fdbU5my3I z1TBi9JT-f=qS!BXZAdQ5G(jL*+X(MI9fnb1pp|x z_B>yZ;9fqdXcU*?dH1Pl@ZgG9ix`qq4^b)ok&v)`INq-WPKgo$+> zHEQ_Cks}j&dfNu~7{P+CW#pH(2$stiFJ+ZfAv@e>@{0|4!jpz~)y=L0}-13*ZD04079 zprk@9H!@(-z$G0h1eQfTN9c~3&`tt8nReZ<{FvZ~#lh>(P~kHRb4(nTh@H{+Q6qX8 zjmBPMCJmWNGGm1UJlm58M_|X;Wyp}{-fteI`4%E-c(?;Gdwx^qLINLK=?uWAK z_Vs0OyZn8%u^DPiYJcqAUcKM$;ayGY7x1rszbeT@oA8$%O{iz_$=c26arAj=>cT~f zmMme==AlFH>=nMkFEr6i}kfsCW%3yAl zitl2!;(Q1}5(Zdl=vPBWGEExWBWPxUghx6Ykqc{ZafJZ|mvkW12o_nXIQ0McH4-1_ z46Mw+8u$Qfpfj-2xz+Pc^>6v!0gD0FIQ#=WxWnJa=^tx=u=8j~7_Q1a(H6#Qct}tf zl=8N{`;J0H-_+r8<7eWS@xqAi?ITd%*74)JF5NgwS)EZWXGAO zeuS?es!BBsD`*&2P#ka-hZW>cUJG910e4s~SWvAc2B4MDOQ_TY^;n5IofergxwUq7 zGKCSMGYdALF3&!zP-uI`^@bfHY*5c|@T$(?|Ct#_`4>1Bsh=Mc=ZA71Xyc%To`gCB zVqK26IoU}iKzluO_=Z{V#{vor$i8}*xvX4y;>52%{CwcoUw%3G%eOxwL3Zb!PnX3( zkKw0!$?Bzhjvf19J-*CrHVP`WPnciUwPZNTLRutP z9aujk+Gvi@faoPu+rkb4ZS7#0Yf5gdF`bN>RMcDt>(QUw6}-L#?yxejc1Rq8+E5da z)(aen|HD_@=UxQ+0S|F6f6lya0?Oq6<|~^92M)%uKuiplDA7dr7Qi37O|wZ&<|D9G zl>fupCa%hCdk(|4IWZABZD7waailZiWDc{&3~b+K=)@@_Vmbjcp^?2Aia1p$%F7J~ zT{%d

=;034=|=!0qfLoVZ{M)EKH3o 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 literal 0 HcmV?d00001 diff --git a/client/ui/frontend/src/assets/fonts/jetbrains-mono-variable.ttf b/client/ui/frontend/src/assets/fonts/jetbrains-mono-variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b60e77f5dbf5505436c1904cb7a9ac4111ee76d0 GIT binary patch literal 303144 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


Y4US@-H<3gJcZD@1xi5O_ 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
+
+ + {t("settings.general.language.help")} +
+
+ + + + + + + + +
+
+ + +
+
+ + + + + +
+ {t("settings.general.language.empty")} +
+
+ + {sorted.map((lang) => { + const checked = lang.code === i18n.language; + return ( + void select(lang.code)} + className={cn( + "my-0.5 flex cursor-default items-center gap-2 rounded-md px-2 py-2 outline-none", + "text-xs font-semibold text-nb-gray-200", + "data-[selected=true]:bg-nb-gray-850 data-[selected=true]:text-nb-gray-50", + )} + > + + {labelFor(lang)} + + + {checked && ( + + )} + + + ); + })} +
+
+ + + +
+
+
+
+
+
+
+ ); +} diff --git a/client/ui/frontend/src/components/ManagementServerSwitch.tsx b/client/ui/frontend/src/components/ManagementServerSwitch.tsx new file mode 100644 index 000000000..0083a767a --- /dev/null +++ b/client/ui/frontend/src/components/ManagementServerSwitch.tsx @@ -0,0 +1,38 @@ +import { useTranslation } from "react-i18next"; +import netbirdLogo from "@/assets/logos/netbird.svg"; +import { SwitchItem } from "@/components/switches/SwitchItem"; +import { SwitchItemGroup } from "@/components/switches/SwitchItemGroup"; +import { ManagementMode } from "@/hooks/useManagementUrl.ts"; + +type Props = { + value: ManagementMode; + onChange: (mode: ManagementMode) => void; + fullWidth?: boolean; +}; + +export const ManagementServerSwitch = ({ value, onChange, fullWidth = false }: Props) => { + const { t, i18n } = useTranslation(); + const itemClass = fullWidth ? "flex-1" : undefined; + return ( + onChange(v as ManagementMode)} + aria-label={t("settings.general.management.label")} + className={fullWidth ? "w-full" : undefined} + > + + {""} + {t("settings.general.management.cloud")} + + + {t("settings.general.management.selfHosted")} + + + ); +}; diff --git a/client/ui/frontend/src/components/SquareIcon.tsx b/client/ui/frontend/src/components/SquareIcon.tsx new file mode 100644 index 000000000..e904d2de5 --- /dev/null +++ b/client/ui/frontend/src/components/SquareIcon.tsx @@ -0,0 +1,37 @@ +import { type ComponentType } from "react"; +import { type LucideProps } from "lucide-react"; +import { cn } from "@/lib/cn"; + +export type SquareIconVariant = "default" | "info" | "warning" | "danger"; + +const variantClass: Record = { + default: "text-white", + info: "text-sky-400", + warning: "text-netbird", + danger: "text-red-500", +}; + +type SquareIconProps = { + icon: ComponentType; + iconSize?: number; + variant?: SquareIconVariant; + className?: string; +}; + +export const SquareIcon = ({ + icon: Icon, + iconSize = 18, + variant = "default", + className, +}: SquareIconProps) => ( +
+ +
+); diff --git a/client/ui/frontend/src/components/Tooltip.tsx b/client/ui/frontend/src/components/Tooltip.tsx new file mode 100644 index 000000000..2c77ba139 --- /dev/null +++ b/client/ui/frontend/src/components/Tooltip.tsx @@ -0,0 +1,98 @@ +import { type ReactNode, useEffect, useRef, useState } from "react"; +import * as RTooltip from "@radix-ui/react-tooltip"; +import { cn } from "@/lib/cn"; + +type Props = { + content: ReactNode; + children: ReactNode; + side?: RTooltip.TooltipContentProps["side"]; + align?: RTooltip.TooltipContentProps["align"]; + delayDuration?: number; + sideOffset?: number; + alignOffset?: number; + interactive?: boolean; + keepOpenOnClick?: boolean; + contentClassName?: string; + closeDelay?: number; +}; + +export const Tooltip = ({ + content, + children, + side = "bottom", + align = "center", + delayDuration = 200, + sideOffset = 6, + alignOffset = 0, + interactive = false, + keepOpenOnClick = true, + contentClassName, + closeDelay = 0, +}: Props) => { + const [open, setOpen] = useState(false); + const hoveringRef = useRef(false); + const closeTimer = useRef | null>(null); + + const cancelClose = () => { + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = null; + } + }; + const scheduleClose = () => { + cancelClose(); + if (closeDelay <= 0) { + setOpen(false); + return; + } + closeTimer.current = setTimeout(() => setOpen(false), closeDelay); + }; + useEffect(() => () => cancelClose(), []); + + const handleOpenChange = (next: boolean) => { + if (!next && keepOpenOnClick && hoveringRef.current) return; + if (next) cancelClose(); + setOpen(next); + }; + + return ( + + + { + hoveringRef.current = true; + cancelClose(); + }} + onPointerLeave={() => { + hoveringRef.current = false; + scheduleClose(); + }} + > + {children} + + + e.preventDefault()} + className={cn( + "z-50 select-none text-xs text-nb-gray-100 shadow-lg", + "data-[state=delayed-open]:animate-in data-[state=closed]:animate-out", + "data-[state=closed]:fade-out-0 data-[state=delayed-open]:fade-in-0", + !interactive && "pointer-events-none", + contentClassName ?? + "rounded-md border border-nb-gray-850 bg-nb-gray-900 px-2 py-1", + )} + > + {content} + + + + + ); +}; diff --git a/client/ui/frontend/src/components/TruncatedText.tsx b/client/ui/frontend/src/components/TruncatedText.tsx new file mode 100644 index 000000000..5b2d2160c --- /dev/null +++ b/client/ui/frontend/src/components/TruncatedText.tsx @@ -0,0 +1,32 @@ +import { useLayoutEffect, useRef, useState, type ReactNode } from "react"; +import { Tooltip } from "@/components/Tooltip"; + +type Props = { + text: string; + className?: string; + tooltipContent?: ReactNode; + delayDuration?: number; +}; + +export const TruncatedText = ({ text, className, tooltipContent, delayDuration = 600 }: Props) => { + const ref = useRef(null); + const [overflowing, setOverflowing] = useState(false); + + useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + setOverflowing(el.scrollWidth > el.clientWidth); + }, [text]); + + const span = ( + + {text} + + ); + if (!overflowing) return span; + return ( + + {span} + + ); +}; diff --git a/client/ui/frontend/src/components/VerticalTabs.tsx b/client/ui/frontend/src/components/VerticalTabs.tsx new file mode 100644 index 000000000..1aedf82a6 --- /dev/null +++ b/client/ui/frontend/src/components/VerticalTabs.tsx @@ -0,0 +1,98 @@ +import { type ComponentType, type ReactNode, forwardRef } from "react"; +import * as Tabs from "@radix-ui/react-tabs"; +import { type LucideProps } from "lucide-react"; +import { cn } from "@/lib/cn"; +import { useFocusVisible } from "@/hooks/useFocusVisible"; + +const Root = forwardRef>( + function VerticalTabsRoot({ className, ...props }, ref) { + return ( + + ); + }, +); + +const List = forwardRef(function VerticalTabsList( + { className, ...props }, + ref, +) { + return ( + + ); +}); + +type TriggerProps = Tabs.TabsTriggerProps & { + icon: ComponentType; + title: string; + iconSize?: number; + adornment?: ReactNode; +}; + +const Trigger = forwardRef(function VerticalTabsTrigger( + { icon: Icon, title, iconSize = 16, adornment, className, ...props }, + ref, +) { + const isFocusVisible = useFocusVisible(); + return ( + + + + {title} + + {adornment && ( +
+ {adornment} +
+ )} +
+ ); +}); + +const Content = forwardRef(function VerticalTabsContent( + { className, ...props }, + ref, +) { + return ( + + ); +}); + +export const VerticalTabs = Object.assign(Root, { List, Trigger, Content }); diff --git a/client/ui/frontend/src/components/buttons/Button.tsx b/client/ui/frontend/src/components/buttons/Button.tsx new file mode 100644 index 000000000..6b151c17b --- /dev/null +++ b/client/ui/frontend/src/components/buttons/Button.tsx @@ -0,0 +1,195 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import { Check, Copy, Loader2 } from "lucide-react"; +import { type ButtonHTMLAttributes, forwardRef, useEffect, useRef, useState } from "react"; + +import { cn } from "@/lib/cn"; + +type ButtonVariants = VariantProps; + +interface ButtonProps extends ButtonHTMLAttributes, ButtonVariants { + disabled?: boolean; + stopPropagation?: boolean; + copy?: string; + loading?: boolean; +} + +const buttonVariants = cva( + [ + "relative", + "cursor-default select-none whitespace-nowrap text-sm font-medium shadow-sm focus:z-10 focus:outline-none focus:ring-2", + "inline-flex items-center justify-center gap-2 transition-colors focus:ring-offset-1", + "disabled:cursor-not-allowed disabled:opacity-40 dark:ring-offset-neutral-950/50 disabled:dark:text-nb-gray-300", + ], + { + variants: { + variant: { + default: [ + "border-gray-200 bg-white text-gray-900 hover:bg-gray-100 hover:text-black focus:ring-zinc-200/50", + "dark:border-gray-700/30 dark:bg-nb-gray dark:text-gray-400 dark:hover:bg-zinc-800/50 dark:hover:text-white dark:focus:ring-zinc-800/50", + ], + primary: [ + "dark:text-gray-100 dark:ring-offset-neutral-950/50 dark:focus:ring-netbird-600/50 enabled:dark:bg-netbird enabled:dark:hover:bg-netbird-500/80 enabled:dark:hover:text-white disabled:dark:bg-nb-gray-900", + "enabled:bg-netbird enabled:text-white enabled:hover:bg-netbird-500 enabled:focus:ring-netbird-400/50", + ], + secondary: [ + "border-gray-200 bg-white text-gray-900 hover:bg-gray-100 hover:text-black focus:ring-zinc-200/50", + "dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20", + "dark:border-gray-700/40 dark:bg-nb-gray-920 dark:text-gray-400 dark:hover:bg-nb-gray-910 dark:hover:text-white", + ], + secondaryLighter: [ + "border-gray-200 bg-white text-gray-900 hover:bg-gray-100 hover:text-black focus:ring-zinc-200/50", + "dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20", + "dark:border-gray-700/70 dark:bg-nb-gray-900/70 dark:text-gray-400 dark:hover:bg-nb-gray-800/60 dark:hover:text-white", + ], + subtle: [ + "border-nb-gray-200 bg-nb-gray-50 text-nb-gray-900 hover:bg-nb-gray-100 focus:ring-nb-gray-200/60", + "dark:ring-offset-neutral-950/50 dark:focus:ring-nb-gray-200/40", + "dark:border-nb-gray-200 dark:bg-nb-gray-50 dark:text-nb-gray-900 dark:hover:bg-nb-gray-100 dark:hover:text-nb-gray-950", + ], + input: [ + "border-neutral-200 bg-white text-gray-900 hover:bg-gray-100 hover:text-black focus:ring-zinc-200/50", + "dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20", + "dark:border-nb-gray-700 dark:bg-nb-gray-900 dark:text-gray-400 dark:hover:bg-nb-gray-900/80", + ], + dropdown: [ + "border-neutral-200 bg-white text-gray-900 hover:bg-gray-100 hover:text-black focus:ring-zinc-200/50", + "dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20", + "dark:border-nb-gray-900 dark:bg-nb-gray-900/40 dark:text-gray-400 dark:hover:bg-nb-gray-900/50", + ], + dotted: [ + "border-dashed border-gray-200 bg-white text-gray-900 hover:bg-gray-100 hover:text-black focus:ring-zinc-200/50", + "dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20", + "dark:border-gray-500/40 dark:bg-nb-gray-900/30 dark:text-gray-400 dark:hover:bg-nb-gray-900/50 dark:hover:text-white", + ], + tertiary: [ + "border-gray-200 bg-white text-gray-900 hover:bg-gray-100 hover:text-black focus:ring-zinc-200/50", + "dark:border-gray-700/40 dark:bg-white dark:text-gray-800 dark:hover:bg-neutral-200 dark:focus:ring-zinc-800/50 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300", + ], + white: [ + "border-white bg-white text-gray-800 outline-none hover:bg-neutral-200 focus:ring-white/50 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300", + "disabled:dark:border-nb-gray-900 disabled:dark:bg-nb-gray-900 disabled:dark:text-nb-gray-300", + ], + outline: [ + "border-gray-200 bg-white text-gray-900 hover:bg-gray-100 hover:text-black focus:ring-zinc-200/50", + "dark:border-netbird dark:bg-transparent dark:text-netbird dark:hover:bg-nb-gray-900/30 dark:focus:ring-zinc-800/50", + ], + "danger-outline": [ + "dark:bg-transparent dark:text-red-500 enabled:dark:hover:border-red-800/50 enabled:hover:dark:bg-red-950/50 enabled:dark:focus:bg-red-950/40 enabled:dark:focus:ring-red-800/20", + ], + "danger-text": [ + "rounded-sm !px-0 !py-0 !shadow-none focus:ring-red-500/30 dark:border-transparent dark:bg-transparent dark:text-red-500 dark:ring-offset-neutral-950/50 dark:hover:text-red-600", + ], + "default-outline": [ + "dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20", + "dark:border-transparent dark:bg-transparent dark:text-nb-gray-400 dark:hover:border-nb-gray-800/50 dark:hover:bg-nb-gray-900/30 dark:hover:text-white", + "data-[state=open]:dark:border-nb-gray-800/50 data-[state=open]:dark:bg-nb-gray-900/30 data-[state=open]:dark:text-white", + ], + ghost: [ + "dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20", + "dark:border-transparent dark:bg-transparent dark:text-nb-gray-400 dark:hover:bg-nb-gray-900/30 dark:hover:text-white", + ], + danger: [ + "dark:bg-red-600 dark:text-red-100 dark:hover:border-red-800/50 hover:dark:bg-red-700 dark:focus:bg-red-700 dark:focus:ring-red-700/20", + ], + }, + size: { + xs: "px-3.5 py-2.5 text-xs", + xs2: "px-4 py-[1.1rem] text-[0.78rem] leading-[0]", + sm: "px-4 py-[9px] text-sm", + md: "px-4 py-[9px]", + lg: "px-4 py-[9px] text-lg", + }, + rounded: { + true: "rounded-md", + false: "", + }, + border: { + 0: "border", + 1: "border border-transparent", + 2: "border border-b-0 border-t-0", + }, + }, + }, +); + +export const Button = forwardRef(function Button( + { + variant = "default", + rounded = true, + border = 1, + size = "md", + stopPropagation = true, + type = "button", + children, + className, + onClick, + disabled, + copy, + loading = false, + ...props + }, + ref, +) { + const [copied, setCopied] = useState(false); + const copyTimer = useRef | null>(null); + useEffect( + () => () => { + if (copyTimer.current) clearTimeout(copyTimer.current); + }, + [], + ); + const iconSize = size === "xs" ? 12 : 14; + return ( + + ); +}); + +export default Button; diff --git a/client/ui/frontend/src/components/buttons/IconButton.tsx b/client/ui/frontend/src/components/buttons/IconButton.tsx new file mode 100644 index 000000000..3d36bc111 --- /dev/null +++ b/client/ui/frontend/src/components/buttons/IconButton.tsx @@ -0,0 +1,36 @@ +import { type ButtonHTMLAttributes, type ComponentType, forwardRef } from "react"; +import { type LucideProps } from "lucide-react"; +import { useFocusVisible } from "@/hooks/useFocusVisible"; +import { cn } from "@/lib/cn"; + +type Props = ButtonHTMLAttributes & { + icon: ComponentType; + iconSize?: number; + iconClassName?: string; +}; + +export const IconButton = forwardRef(function IconButton( + { icon: Icon, iconSize = 17, iconClassName, className, type = "button", disabled, ...props }, + ref, +) { + const isFocusVisible = useFocusVisible(); + return ( + + ); +}); diff --git a/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx b/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx new file mode 100644 index 000000000..caf98c2c8 --- /dev/null +++ b/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx @@ -0,0 +1,35 @@ +import { type ReactNode, forwardRef } from "react"; +import { cn } from "@/lib/cn.ts"; +import { isMacOS } from "@/lib/platform.ts"; + +type ConfirmDialogProps = { + children: ReactNode; + "aria-label"?: string; + "aria-labelledby"?: string; +}; + +export const ConfirmDialog = forwardRef(function ConfirmDialog( + { children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy }, + ref, +) { + return ( + +
+ {children} +
+
+ ); +}); diff --git a/client/ui/frontend/src/components/dialog/ConfirmModal.tsx b/client/ui/frontend/src/components/dialog/ConfirmModal.tsx new file mode 100644 index 000000000..241a69ec9 --- /dev/null +++ b/client/ui/frontend/src/components/dialog/ConfirmModal.tsx @@ -0,0 +1,84 @@ +import { type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import * as Dialog from "@/components/dialog/Dialog"; +import { Button } from "@/components/buttons/Button"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogActions } from "@/components/dialog/DialogActions"; + +type ConfirmModalProps = { + open: boolean; + title: ReactNode; + description: ReactNode; + confirmLabel: string; + cancelLabel?: string; + danger?: boolean; + busy?: boolean; + onConfirm: () => void; + onCancel: () => void; +}; + +export const ConfirmModal = ({ + open, + title, + description, + confirmLabel, + cancelLabel, + danger = false, + busy = false, + onConfirm, + onCancel, +}: ConfirmModalProps) => { + const { t } = useTranslation(); + const resolvedCancel = cancelLabel ?? t("common.cancel"); + + const srTitle = typeof title === "string" ? title : undefined; + const srDescription = typeof description === "string" ? description : undefined; + + return ( + { + if (!next && !busy) onCancel(); + }} + > + e.preventDefault()} + > +
+
+ {title} + + {description} + +
+ + + + + +
+
+
+ ); +}; diff --git a/client/ui/frontend/src/components/dialog/Dialog.tsx b/client/ui/frontend/src/components/dialog/Dialog.tsx new file mode 100644 index 000000000..fa8007d9f --- /dev/null +++ b/client/ui/frontend/src/components/dialog/Dialog.tsx @@ -0,0 +1,159 @@ +import { + forwardRef, + type ComponentPropsWithoutRef, + type ElementRef, + type HTMLAttributes, +} from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; +import { X } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/cn"; + +export const Root = DialogPrimitive.Root; + +type OverlayProps = ComponentPropsWithoutRef & { + exitAnimation?: boolean; +}; + +const Overlay = forwardRef, OverlayProps>( + function DialogOverlay({ className, exitAnimation = false, ...props }, ref) { + return ( + + ); + }, +); + +type ContentProps = ComponentPropsWithoutRef & { + showClose?: boolean; + maxWidthClass?: string; + exitAnimation?: boolean; + srTitle?: string; + srDescription?: string; +}; + +export const Content = forwardRef, ContentProps>( + function DialogContent( + { + className, + children, + showClose = true, + maxWidthClass = "max-w-md", + exitAnimation = false, + srTitle, + srDescription, + ...props + }, + ref, + ) { + const { t } = useTranslation(); + return ( + + + e.stopPropagation()} + {...props} + > + + + {srTitle ?? t("common.netbird")} + + + {srDescription && ( + + + {srDescription} + + + )} + {children} + {showClose && ( + + + + )} + + + + ); + }, +); + +export const Title = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(function DialogTitle({ className, ...props }, ref) { + return ( + + ); +}); + +export const Description = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(function DialogDescription({ className, ...props }, ref) { + return ( + + ); +}); + +type FooterProps = HTMLAttributes & { + separator?: boolean; +}; + +export const Footer = ({ className, separator = true, ...props }: FooterProps) => ( +
+
*]:w-full sm:[&>*]:w-auto", + "px-8 pt-6", + className, + )} + {...props} + /> +
+); diff --git a/client/ui/frontend/src/components/dialog/DialogActions.tsx b/client/ui/frontend/src/components/dialog/DialogActions.tsx new file mode 100644 index 000000000..3aa3a1fe5 --- /dev/null +++ b/client/ui/frontend/src/components/dialog/DialogActions.tsx @@ -0,0 +1,13 @@ +import { type ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +type DialogActionsProps = { + children: ReactNode; + className?: string; +}; + +export const DialogActions = ({ children, className }: DialogActionsProps) => ( +
+ {children} +
+); diff --git a/client/ui/frontend/src/components/dialog/DialogDescription.tsx b/client/ui/frontend/src/components/dialog/DialogDescription.tsx new file mode 100644 index 000000000..12c358043 --- /dev/null +++ b/client/ui/frontend/src/components/dialog/DialogDescription.tsx @@ -0,0 +1,26 @@ +import { type ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +type DialogAlign = "left" | "center" | "right"; + +const alignClass: Record = { + left: "text-left", + center: "text-center", + right: "text-right", +}; + +type DialogDescriptionProps = { + children: ReactNode; + className?: string; + align?: DialogAlign; +}; + +export const DialogDescription = ({ + children, + className, + align = "center", +}: DialogDescriptionProps) => ( +

+ {children} +

+); diff --git a/client/ui/frontend/src/components/dialog/DialogHeading.tsx b/client/ui/frontend/src/components/dialog/DialogHeading.tsx new file mode 100644 index 000000000..b9dda72a9 --- /dev/null +++ b/client/ui/frontend/src/components/dialog/DialogHeading.tsx @@ -0,0 +1,35 @@ +import { type ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +type DialogAlign = "left" | "center" | "right"; + +const alignClass: Record = { + left: "text-left", + center: "text-center", + right: "text-right", +}; + +type DialogHeadingProps = { + children: ReactNode; + className?: string; + align?: DialogAlign; + id?: string; +}; + +export const DialogHeading = ({ + children, + className, + align = "center", + id, +}: DialogHeadingProps) => ( +

+ {children} +

+); diff --git a/client/ui/frontend/src/components/empty-state/DaemonOutdatedOverlay.tsx b/client/ui/frontend/src/components/empty-state/DaemonOutdatedOverlay.tsx new file mode 100644 index 000000000..4ee8c2740 --- /dev/null +++ b/client/ui/frontend/src/components/empty-state/DaemonOutdatedOverlay.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from "react-i18next"; +import { AlertTriangleIcon, DownloadIcon } from "lucide-react"; +import { Browser } from "@wailsio/runtime"; +import { Button } from "@/components/buttons/Button"; +import { useStatus } from "@/contexts/StatusContext.tsx"; + +const RELEASES_URL = "https://github.com/netbirdio/netbird/releases/latest"; + +function openUrl(url: string) { + Browser.OpenURL(url).catch(() => globalThis.open(url, "_blank")); +} + +export const DaemonOutdatedOverlay = () => { + const { t } = useTranslation(); + const { isDaemonOutdated } = useStatus(); + + if (!isDaemonOutdated) return null; + + return ( +
+
+
+ +
+ +
+

+ {t("daemon.outdated.title")} +

+

{t("daemon.outdated.description")}

+
+ +
+ +
+
+
+ ); +}; diff --git a/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx b/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx new file mode 100644 index 000000000..89c121e21 --- /dev/null +++ b/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx @@ -0,0 +1,52 @@ +import { useTranslation } from "react-i18next"; +import { AlertCircleIcon, BookText } from "lucide-react"; +import { Browser } from "@wailsio/runtime"; +import { Button } from "@/components/buttons/Button"; +import { useStatus } from "@/contexts/StatusContext.tsx"; + +const DOCS_URL = "https://docs.netbird.io/how-to/installation"; + +function openUrl(url: string) { + Browser.OpenURL(url).catch(() => globalThis.open(url, "_blank")); +} + +export const DaemonUnavailableOverlay = () => { + const { t } = useTranslation(); + const { isDaemonUnavailable } = useStatus(); + + if (!isDaemonUnavailable) return null; + + return ( +
+
+
+ +
+ +
+

+ {t("daemon.unavailable.title")} +

+

+ {t("daemon.unavailable.description")} +

+
+ +
+ +
+
+
+ ); +}; diff --git a/client/ui/frontend/src/components/empty-state/EmptyState.tsx b/client/ui/frontend/src/components/empty-state/EmptyState.tsx new file mode 100644 index 000000000..7d6890a98 --- /dev/null +++ b/client/ui/frontend/src/components/empty-state/EmptyState.tsx @@ -0,0 +1,31 @@ +import { type ComponentType } from "react"; +import { type LucideProps } from "lucide-react"; +import { cn } from "@/lib/cn"; +import { SquareIcon } from "@/components/SquareIcon"; +import { isMacOS } from "@/lib/platform"; + +// Knob to shift the centered main-window content up/down together. +export const contentVerticalOffset = (): string => (isMacOS() ? "0.6rem" : "-1.4rem"); +export const contentTop = (base: string) => `calc(${base} + ${contentVerticalOffset()})`; + +type Props = { + icon: ComponentType; + title: string; + description?: string; + className?: string; +}; + +export const EmptyState = ({ icon, title, description, className }: Props) => { + return ( +
+
+ +

{title}

+ {description &&

{description}

} +
+
+ ); +}; diff --git a/client/ui/frontend/src/components/empty-state/NoResults.tsx b/client/ui/frontend/src/components/empty-state/NoResults.tsx new file mode 100644 index 000000000..cf0995b37 --- /dev/null +++ b/client/ui/frontend/src/components/empty-state/NoResults.tsx @@ -0,0 +1,22 @@ +import { type ComponentType } from "react"; +import { FunnelXIcon, type LucideProps } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { EmptyState } from "./EmptyState"; + +type Props = { + icon?: ComponentType; + title?: string; + description?: string; +}; + +export const NoResults = ({ icon = FunnelXIcon, title, description }: Props) => { + const { t } = useTranslation(); + return ( + + ); +}; diff --git a/client/ui/frontend/src/components/empty-state/NotConnectedState.tsx b/client/ui/frontend/src/components/empty-state/NotConnectedState.tsx new file mode 100644 index 000000000..2bcf70376 --- /dev/null +++ b/client/ui/frontend/src/components/empty-state/NotConnectedState.tsx @@ -0,0 +1,16 @@ +import { GlobeOffIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { EmptyState } from "./EmptyState"; + +export const NotConnectedState = () => { + const { t } = useTranslation(); + return ( +
+ +
+ ); +}; diff --git a/client/ui/frontend/src/components/inputs/Input.tsx b/client/ui/frontend/src/components/inputs/Input.tsx new file mode 100644 index 000000000..2dad80d7a --- /dev/null +++ b/client/ui/frontend/src/components/inputs/Input.tsx @@ -0,0 +1,374 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import { Check, ChevronDown, ChevronUp, Copy, Eye, EyeOff } from "lucide-react"; +import { + forwardRef, + type InputHTMLAttributes, + type ReactNode, + useEffect, + useId, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/cn"; +import { Label } from "@/components/typography/Label"; + +type InputVariants = VariantProps; + +export interface InputProps extends InputHTMLAttributes, InputVariants { + label?: string; + customPrefix?: ReactNode; + customSuffix?: ReactNode; + maxWidthClass?: string; + icon?: ReactNode; + error?: string; + warning?: string; + prefixClassName?: string; + showPasswordToggle?: boolean; + copy?: boolean; +} + +const inputVariants = cva("", { + variants: { + variant: { + default: [ + "border-neutral-200 placeholder:text-neutral-500 dark:border-nb-gray-700 dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70", + "ring-offset-neutral-200/20 focus-visible:ring-neutral-300/10 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20", + ], + darker: [ + "border-neutral-300 placeholder:text-neutral-500 dark:border-nb-gray-800 dark:bg-nb-gray-920 dark:placeholder:text-neutral-400/70", + "ring-offset-neutral-200/20 focus-visible:ring-neutral-300/10 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20", + ], + error: [ + "border-neutral-200 text-red-500 placeholder:text-neutral-500 dark:border-red-500 dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70", + "ring-offset-red-500/10 focus-visible:ring-red-500/10 dark:ring-offset-red-500/10 dark:focus-visible:ring-red-500/10", + ], + warning: [ + "border-neutral-200 text-orange-400 placeholder:text-neutral-500 dark:border-orange-400 dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70", + "ring-offset-orange-400/10 focus-visible:ring-orange-400/10 dark:ring-offset-orange-400/10 dark:focus-visible:ring-orange-400/10", + ], + }, + prefixSuffixVariant: { + default: [ + "border-neutral-200 text-nb-gray-300 dark:border-nb-gray-700 dark:bg-nb-gray-900", + ], + error: ["border-red-500 text-nb-gray-300 text-red-500 dark:bg-nb-gray-900"], + }, + }, +}); + +function computeNextStepValue(el: HTMLInputElement, delta: 1 | -1): number { + const stepAttr = el.step === "" ? 1 : Number(el.step); + const step = Number.isFinite(stepAttr) && stepAttr > 0 ? stepAttr : 1; + const min = el.min === "" ? -Infinity : Number(el.min); + const max = el.max === "" ? Infinity : Number(el.max); + const current = el.value === "" ? 0 : Number(el.value); + let next = (Number.isFinite(current) ? current : 0) + delta * step; + if (next < min) next = min; + if (next > max) next = max; + return next; +} + +function buildInputClassName( + opts: Readonly<{ + variant: InputVariants["variant"]; + hasCustomPrefix: boolean; + hasSuffix: boolean; + hasIcon: boolean; + readOnly?: boolean; + showStepper: boolean; + className?: string; + }>, +): string { + return cn( + inputVariants({ variant: opts.variant }), + "flex h-[40px] w-full select-text rounded-md bg-white px-3 py-2 text-sm", + "file:border-0 file:bg-transparent file:text-sm file:font-medium", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2", + "disabled:cursor-not-allowed disabled:opacity-40", + opts.hasCustomPrefix && "!rounded-l-none !border-l-0", + opts.hasSuffix && "!pr-9", + opts.hasIcon && "!pl-10", + "border", + opts.readOnly && "!border-nb-gray-800 !bg-nb-gray-910 text-nb-gray-350", + opts.showStepper && + "!rounded-r-none [-moz-appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none", + opts.className, + ); +} + +function InputAffix({ + content, + error, + disabled, + className, +}: Readonly<{ content: ReactNode; error?: string; disabled?: boolean; className?: string }>) { + return ( +
+ {content} +
+ ); +} + +function InputIconSlot({ icon, disabled }: Readonly<{ icon: ReactNode; disabled?: boolean }>) { + return ( +
+ {icon} +
+ ); +} + +function InputSuffixSlot({ + suffix, + disabled, +}: Readonly<{ suffix: ReactNode; disabled?: boolean }>) { + return ( +
+ {suffix} +
+ ); +} + +function NumberStepper({ + error, + disabled, + onStep, +}: Readonly<{ error?: string; disabled?: boolean; onStep: (delta: 1 | -1) => void }>) { + const { t } = useTranslation(); + return ( +
+ + +
+ ); +} + +function FieldMessage({ + id, + error, + warning, +}: Readonly<{ id?: string; error?: string; warning?: string }>) { + if (!error && !warning) return null; + return ( + + {error ?? warning} + + ); +} + +export const Input = forwardRef(function Input( + { + className, + type, + label, + customSuffix, + customPrefix, + icon, + maxWidthClass = "", + error, + warning, + variant = "default", + prefixClassName, + showPasswordToggle = false, + copy = false, + id, + ...props + }, + ref, +) { + const { t } = useTranslation(); + const [showPassword, setShowPassword] = useState(false); + const [copied, setCopied] = useState(false); + const isPasswordType = type === "password"; + const inputType = isPasswordType && showPassword ? "text" : type; + const isNumber = type === "number"; + + const reactId = useId(); + const fallbackId = `input-${reactId}`; + const inputId = id ?? (label ? fallbackId : undefined); + const messageId = error || warning ? `${inputId ?? fallbackId}-message` : undefined; + + const copyTimer = useRef | null>(null); + useEffect( + () => () => { + if (copyTimer.current) clearTimeout(copyTimer.current); + }, + [], + ); + + const internalRef = useRef(null); + const setRefs = (el: HTMLInputElement | null) => { + internalRef.current = el; + if (typeof ref === "function") ref(el); + else if (ref) ref.current = el; + }; + + const stepBy = (delta: 1 | -1) => { + const el = internalRef.current; + if (!el || el.disabled || el.readOnly) return; + const setter = Object.getOwnPropertyDescriptor( + globalThis.HTMLInputElement.prototype, + "value", + )?.set; + const next = computeNextStepValue(el, delta); + setter?.call(el, String(next)); + el.dispatchEvent(new Event("input", { bubbles: true })); + }; + + const passwordToggle = + isPasswordType && showPasswordToggle ? ( + + ) : null; + + const onCopy = async () => { + const text = props.value == null ? (internalRef.current?.value ?? "") : String(props.value); + if (!text) return; + try { + await navigator.clipboard.writeText(text); + setCopied(true); + if (copyTimer.current) clearTimeout(copyTimer.current); + copyTimer.current = setTimeout(() => setCopied(false), 1500); + } catch (e) { + console.warn("copy to clipboard failed", e); + } + }; + + const copyToggle = copy ? ( + + ) : null; + + const suffix = passwordToggle || copyToggle || customSuffix; + const showStepper = isNumber; + const warningVariant = warning ? "warning" : variant; + const resolvedVariant = error ? "error" : warningVariant; + + const inputClassName = buildInputClassName({ + variant: resolvedVariant, + hasCustomPrefix: !!customPrefix, + hasSuffix: !!suffix, + hasIcon: !!icon, + readOnly: props.readOnly, + showStepper, + className, + }); + + return ( +
+ {label && } +
+ {customPrefix && ( + + )} + + {icon && } + +
+ + + {suffix && } +
+ + {showStepper && ( + + )} +
+ +
+ ); +}); + +export default Input; diff --git a/client/ui/frontend/src/components/inputs/SearchInput.tsx b/client/ui/frontend/src/components/inputs/SearchInput.tsx new file mode 100644 index 000000000..5f46e8fba --- /dev/null +++ b/client/ui/frontend/src/components/inputs/SearchInput.tsx @@ -0,0 +1,59 @@ +import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { SearchIcon } from "lucide-react"; +import { cn } from "@/lib/cn"; + +type Props = InputHTMLAttributes & { + iconSize?: number; + shortcut?: ReactNode; +}; + +export const SearchInput = forwardRef(function SearchInput( + { iconSize = 16, className, disabled, shortcut, "aria-label": ariaLabel, ...props }, + ref, +) { + const { t } = useTranslation(); + return ( +
+ + + {shortcut && ( + + {shortcut} + + )} +
+ ); +}); diff --git a/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx b/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx new file mode 100644 index 000000000..45e3e333a --- /dev/null +++ b/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { HelpText } from "@/components/typography/HelpText"; +import { Label } from "@/components/typography/Label"; +import { ToggleSwitch } from "@/components/switches/ToggleSwitch"; +import { cn } from "@/lib/cn"; + +interface Props { + value: boolean; + onChange: (value: boolean) => void; + helpText?: React.ReactNode; + label?: React.ReactNode; + children?: React.ReactNode; + disabled?: boolean; + loading?: boolean; + dataCy?: string; + className?: string; + labelClassName?: string; + textWrapperClassName?: string; +} + +export default function FancyToggleSwitch({ + value, + onChange, + helpText, + label, + children, + disabled = false, + loading = false, + dataCy, + className, + labelClassName, + textWrapperClassName = "max-w-lg", +}: Readonly) { + const switchId = React.useId(); + const descriptionId = React.useId(); + + if (loading) { + const shimmer = + "text-transparent select-none rounded bg-[#25282d] box-decoration-clone animate-pulse"; + return ( +
+
+
+ + + + {helpText} + + +
+
+
+
+
+
+ ); + } + + return ( +
+
+
+ + + {helpText} + +
+
+ +
+
+ {children && value ?
{children}
: null} +
+ ); +} diff --git a/client/ui/frontend/src/components/switches/SwitchItem.tsx b/client/ui/frontend/src/components/switches/SwitchItem.tsx new file mode 100644 index 000000000..e23c73fe3 --- /dev/null +++ b/client/ui/frontend/src/components/switches/SwitchItem.tsx @@ -0,0 +1,42 @@ +import * as RadioGroup from "@radix-ui/react-radio-group"; +import { motion } from "framer-motion"; +import { type ReactNode } from "react"; +import { cn } from "@/lib/cn"; +import { useSwitchItemGroup } from "@/components/switches/SwitchItemGroup"; + +type Props = { + value: string; + children: ReactNode; + className?: string; +}; + +export const SwitchItem = ({ value, children, className }: Props) => { + const { value: activeValue, layoutId } = useSwitchItemGroup(); + const active = activeValue === value; + + return ( + + {active && ( + + )} + + {children} + + + ); +}; diff --git a/client/ui/frontend/src/components/switches/SwitchItemGroup.tsx b/client/ui/frontend/src/components/switches/SwitchItemGroup.tsx new file mode 100644 index 000000000..b4361d530 --- /dev/null +++ b/client/ui/frontend/src/components/switches/SwitchItemGroup.tsx @@ -0,0 +1,60 @@ +import * as RadioGroup from "@radix-ui/react-radio-group"; +import { createContext, type ReactNode, useContext, useId, useMemo } from "react"; +import { cn } from "@/lib/cn"; + +type SwitchItemGroupContextValue = { + value: string; + layoutId: string; +}; + +const SwitchItemGroupContext = createContext(null); + +export const useSwitchItemGroup = () => { + const ctx = useContext(SwitchItemGroupContext); + if (!ctx) { + throw new Error("SwitchItem must be used inside a SwitchItemGroup"); + } + return ctx; +}; + +type Props = { + value: string; + onChange: (value: string) => void; + children: ReactNode; + className?: string; + disabled?: boolean; + "aria-label"?: string; + "aria-labelledby"?: string; +}; + +export const SwitchItemGroup = ({ + value, + onChange, + children, + className, + disabled = false, + "aria-label": ariaLabel, + "aria-labelledby": ariaLabelledBy, +}: Props) => { + const layoutId = useId(); + const contextValue = useMemo(() => ({ value, layoutId }), [value, layoutId]); + + return ( + + + {children} + + + ); +}; diff --git a/client/ui/frontend/src/components/switches/ToggleSwitch.tsx b/client/ui/frontend/src/components/switches/ToggleSwitch.tsx new file mode 100644 index 000000000..2d9f597e6 --- /dev/null +++ b/client/ui/frontend/src/components/switches/ToggleSwitch.tsx @@ -0,0 +1,77 @@ +"use client"; + +import * as SwitchPrimitives from "@radix-ui/react-switch"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { cn } from "@/lib/cn"; + +type SwitchVariants = VariantProps; + +const switchVariants = cva("", { + variants: { + size: { + default: "h-[24px] w-[44px]", + small: "h-[18px] w-[36px]", + large: "h-[36px] w-[66px]", + }, + variant: { + default: [ + "dark:data-[state=checked]:bg-netbird dark:data-[state=unchecked]:bg-nb-gray-700", + "dark:data-[state=checked]:hover:bg-netbird-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600", + "data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200", + "data-[state=checked]:hover:bg-neutral-800 data-[state=unchecked]:hover:bg-neutral-300", + ], + "red-green": [ + "dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700", + "dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600", + "data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200", + "data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300", + ], + red: [ + "dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700", + "dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600", + "data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200", + "data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300", + ], + }, + "thumb-size": { + default: + "h-5 w-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0", + small: "h-[14px] w-[14px] data-[state=checked]:translate-x-[17px] data-[state=unchecked]:translate-x-0", + large: "h-[30px] w-[30px] data-[state=checked]:translate-x-[31px] data-[state=unchecked]:translate-x-[1px]", + }, + }, +}); + +const ToggleSwitch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + SwitchVariants & { dataCy?: string } +>(({ className, size = "default", variant = "default", dataCy, disabled, ...props }, ref) => ( + { + e.stopPropagation(); + props.onClick?.(e); + }} + ref={ref} + > + + +)); +ToggleSwitch.displayName = SwitchPrimitives.Root.displayName; + +export { ToggleSwitch }; diff --git a/client/ui/frontend/src/components/typography/HelpText.tsx b/client/ui/frontend/src/components/typography/HelpText.tsx new file mode 100644 index 000000000..8c52ff714 --- /dev/null +++ b/client/ui/frontend/src/components/typography/HelpText.tsx @@ -0,0 +1,24 @@ +import { type ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +type Props = { + children?: ReactNode; + margin?: boolean; + className?: string; + disabled?: boolean; +}; + +export const HelpText = ({ children, margin = true, className, disabled = false }: Props) => ( + + {children} + +); + +export default HelpText; diff --git a/client/ui/frontend/src/components/typography/Label.tsx b/client/ui/frontend/src/components/typography/Label.tsx new file mode 100644 index 000000000..a8e1a446f --- /dev/null +++ b/client/ui/frontend/src/components/typography/Label.tsx @@ -0,0 +1,42 @@ +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; +import { type ComponentPropsWithoutRef, forwardRef, type Ref } from "react"; +import { cn } from "@/lib/cn"; + +const labelVariants = cva( + "mb-1.5 inline-block flex items-center gap-2 text-sm font-medium leading-none tracking-wider peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-nb-gray-100", +); + +type LabelProps = ComponentPropsWithoutRef & + VariantProps & { + as?: "label" | "div"; + disabled?: boolean; + }; + +export const Label = forwardRef(function Label( + { className, as = "label", disabled = false, children, ...props }, + ref, +) { + const classes = cn( + labelVariants(), + className, + "select-none transition-all duration-300", + disabled && "pointer-events-none opacity-30", + ); + + if (as === "div") { + return ( +
} className={classes}> + {children} +
+ ); + } + + return ( + } className={classes} {...props}> + {children} + + ); +}); + +export default Label; diff --git a/client/ui/frontend/src/contexts/ClientVersionContext.tsx b/client/ui/frontend/src/contexts/ClientVersionContext.tsx new file mode 100644 index 000000000..c0699a9ee --- /dev/null +++ b/client/ui/frontend/src/contexts/ClientVersionContext.tsx @@ -0,0 +1,114 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { Events } from "@wailsio/runtime"; + +import { Update as UpdateSvc, WindowManager } from "@bindings/services"; +import type { State as UpdateState } from "@bindings/updater/models.js"; +import i18next from "@/lib/i18n"; +import { errorDialog, formatErrorMessage } from "@/lib/errors"; + +const isDaemonUnavailable = (e: unknown): boolean => { + const msg = e instanceof Error ? e.message : String(e); + return msg.includes("code = Unavailable"); +}; + +type ClientVersionContextValue = { + updateAvailable: boolean; + updateVersion: string | null; + enforced: boolean; + installing: boolean; + triggerUpdate: () => void; + updating: boolean; +}; + +const EVENT_UPDATE_STATE = "netbird:update:state"; + +const emptyState: UpdateState = { + available: false, + version: "", + enforced: false, + installing: false, +}; + +const ClientVersionContext = createContext(null); + +export const useClientVersion = () => { + const ctx = useContext(ClientVersionContext); + if (!ctx) { + throw new Error("useClientVersion must be used inside ClientVersionProvider"); + } + return ctx; +}; + +export const ClientVersionProvider = ({ children }: { children: ReactNode }) => { + const [state, setState] = useState(emptyState); + const [updating, setUpdating] = useState(false); + + useEffect(() => { + let cancelled = false; + UpdateSvc.GetState() + .then((s) => { + if (cancelled || !s) return; + setState(s); + }) + .catch((e) => { + if (cancelled || isDaemonUnavailable(e)) return; + void errorDialog({ + Title: i18next.t("update.error.loadStateTitle"), + Message: formatErrorMessage(e), + }); + }); + const off = Events.On(EVENT_UPDATE_STATE, (ev: { data: UpdateState }) => { + if (ev?.data) setState(ev.data); + }); + return () => { + cancelled = true; + off?.(); + }; + }, []); + + const prevInstallingRef = useRef(false); + useEffect(() => { + if (state.installing && !prevInstallingRef.current) { + WindowManager.OpenInstallProgress(state.version || "").catch(console.error); + } + prevInstallingRef.current = state.installing; + }, [state.installing, state.version]); + + const triggerUpdate = useCallback(() => { + setUpdating(true); + WindowManager.OpenInstallProgress(state.version || "").catch(console.error); + UpdateSvc.Trigger() + .catch(async (e) => { + if (isDaemonUnavailable(e)) return; + WindowManager.CloseInstallProgress().catch(console.error); + await errorDialog({ + Title: i18next.t("update.error.triggerTitle"), + Message: formatErrorMessage(e), + }); + }) + .finally(() => setUpdating(false)); + }, [state.version]); + + const value = useMemo( + () => ({ + updateAvailable: state.available, + updateVersion: state.version || null, + enforced: state.enforced, + installing: state.installing, + triggerUpdate, + updating, + }), + [state, triggerUpdate, updating], + ); + + return {children}; +}; diff --git a/client/ui/frontend/src/contexts/DebugBundleContext.tsx b/client/ui/frontend/src/contexts/DebugBundleContext.tsx new file mode 100644 index 000000000..a0a131fbf --- /dev/null +++ b/client/ui/frontend/src/contexts/DebugBundleContext.tsx @@ -0,0 +1,314 @@ +import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from "react"; +import { Connection as ConnectionSvc, Debug as DebugSvc } from "@bindings/services"; +import type { DebugBundleResult } from "@bindings/services/models.js"; +import i18next from "@/lib/i18n"; +import { errorDialog, formatErrorMessage } from "@/lib/errors.ts"; +import { startConnection } from "@/lib/connection.ts"; + +const NETBIRD_UPLOAD_URL = "https://upload.debug.netbird.io/upload-url"; +const TRACE_LOG_FILE_COUNT = 5; +const PLAIN_LOG_FILE_COUNT = 1; +const TRACE_LOG_LEVEL = "trace"; +const DEFAULT_LOG_LEVEL = "info"; + +export type DebugStage = + | { kind: "idle" } + | { kind: "preparing-trace" } + | { kind: "reconnecting" } + | { kind: "capturing"; remainingSec: number; totalSec: number } + | { kind: "restoring-level" } + | { kind: "bundling" } + | { kind: "uploading" } + | { kind: "cancelling" } + | { kind: "done"; result: DebugBundleResult; uploadAttempted: boolean }; + +const sleep = (ms: number, signal: AbortSignal) => + new Promise((resolve, reject) => { + if (signal.aborted) { + reject(new DOMException("aborted", "AbortError")); + return; + } + const onAbort = () => { + clearTimeout(id); + reject(new DOMException("aborted", "AbortError")); + }; + const id = setTimeout(() => { + signal.removeEventListener("abort", onAbort); + resolve(); + }, ms); + signal.addEventListener("abort", onAbort); + }); + +const isAbort = (e: unknown) => e instanceof DOMException && e.name === "AbortError"; + +const throwIfAborted = (signal: AbortSignal) => { + if (signal.aborted) throw new DOMException("aborted", "AbortError"); +}; + +const setLogLevelBestEffort = async (level: string) => { + try { + await DebugSvc.SetLogLevel({ level }); + } catch (e) { + console.warn("[DebugBundle] best-effort set log level failed", e); + } +}; + +const stopCaptureBestEffort = async () => { + try { + await DebugSvc.StopBundleCapture(); + } catch (e) { + console.warn("[DebugBundle] best-effort stop packet capture failed", e); + } +}; + +type LevelState = { original: string; raised: boolean }; +type CaptureState = { started: boolean }; + +type BundleOptions = { + trace: boolean; + capture: boolean; + capturePackets: boolean; + hasWindow: boolean; + totalSec: number; + uploadUrl: string; + anonymize: boolean; + systemInfo: boolean; +}; + +const startCaptureBestEffort = async (totalSec: number, pcap: CaptureState) => { + try { + // Mirror the CLI's safety margin: window + 30s, server caps at 10m. + await DebugSvc.StartBundleCapture(totalSec + 30); + pcap.started = true; + } catch (e) { + console.warn("[DebugBundle] start packet capture failed", e); + } +}; + +const cleanupBestEffort = async (pcap: CaptureState, level: LevelState, restoreLevel: boolean) => { + if (pcap.started) { + await stopCaptureBestEffort(); + pcap.started = false; + } + if (restoreLevel && level.raised) { + await setLogLevelBestEffort(level.original); + } +}; + +const raiseToTrace = async ( + signal: AbortSignal, + level: LevelState, + setStage: (s: DebugStage) => void, +) => { + setStage({ kind: "preparing-trace" }); + try { + const cur = await DebugSvc.GetLogLevel(); + if (cur?.level) level.original = cur.level; + } catch (e) { + console.warn("[DebugBundle] read current log level failed", e); + } + throwIfAborted(signal); + await DebugSvc.SetLogLevel({ level: TRACE_LOG_LEVEL }); + level.raised = true; +}; + +const cycleConnection = async (signal: AbortSignal, setStage: (s: DebugStage) => void) => { + throwIfAborted(signal); + setStage({ kind: "reconnecting" }); + try { + await ConnectionSvc.Down(); + } catch (e) { + console.warn("[DebugBundle] disconnect before capture failed", e); + } + throwIfAborted(signal); + await startConnection(undefined, signal); +}; + +const restoreLogLevel = async (level: LevelState, setStage: (s: DebugStage) => void) => { + setStage({ kind: "restoring-level" }); + try { + await DebugSvc.SetLogLevel({ level: level.original }); + level.raised = false; + } catch (e) { + console.warn("[DebugBundle] restore log level failed", e); + } +}; + +const waitCaptureWindow = async ( + signal: AbortSignal, + setStage: (s: DebugStage) => void, + totalSec: number, +) => { + for (let remaining = totalSec; remaining > 0; remaining--) { + setStage({ kind: "capturing", remainingSec: remaining, totalSec }); + await sleep(1000, signal); + } +}; + +const runBundleFlow = async ( + signal: AbortSignal, + opts: BundleOptions, + level: LevelState, + pcap: CaptureState, + setStage: (s: DebugStage) => void, + setLastBundlePath: (p: string) => void, +) => { + if (opts.trace) { + await raiseToTrace(signal, level, setStage); + } + throwIfAborted(signal); + + if (opts.capture) { + await cycleConnection(signal, setStage); + } + throwIfAborted(signal); + + if (opts.hasWindow && opts.capturePackets) { + await startCaptureBestEffort(opts.totalSec, pcap); + } + throwIfAborted(signal); + + if (opts.hasWindow) { + await waitCaptureWindow(signal, setStage, opts.totalSec); + } + + if (pcap.started) { + await stopCaptureBestEffort(); + pcap.started = false; + } + + if (level.raised) { + await restoreLogLevel(level, setStage); + } + + throwIfAborted(signal); + setStage({ kind: "bundling" }); + const logFileCount = opts.trace ? TRACE_LOG_FILE_COUNT : PLAIN_LOG_FILE_COUNT; + + if (opts.uploadUrl) setStage({ kind: "uploading" }); + const result = await DebugSvc.Bundle({ + anonymize: opts.anonymize, + systemInfo: opts.systemInfo, + uploadUrl: opts.uploadUrl, + logFileCount, + }); + throwIfAborted(signal); + if (result.path) setLastBundlePath(result.path); + setStage({ kind: "done", result, uploadAttempted: Boolean(opts.uploadUrl) }); +}; + +const useDebugBundle = () => { + const [anonymize, setAnonymize] = useState(false); + const [systemInfo, setSystemInfo] = useState(true); + const [upload, setUpload] = useState(true); + const [trace, setTrace] = useState(true); + const [capture, setCapture] = useState(false); + const [traceMinutes, setTraceMinutes] = useState(1); + const [capturePackets, setCapturePackets] = useState(true); + const [stage, setStage] = useState({ kind: "idle" }); + const [lastBundlePath, setLastBundlePath] = useState(""); + const abortRef = useRef(null); + + useEffect(() => { + return () => { + abortRef.current?.abort(); + }; + }, []); + + const isRunning = stage.kind !== "idle" && stage.kind !== "done"; + + const reset = () => setStage({ kind: "idle" }); + + const cancel = () => { + if (!abortRef.current || abortRef.current.signal.aborted) return; + abortRef.current.abort(); + setStage({ kind: "cancelling" }); + }; + + const run = async () => { + const ctrl = new AbortController(); + abortRef.current = ctrl; + const signal = ctrl.signal; + + const totalSec = Math.max(1, Math.min(30, traceMinutes)) * 60; + const level: LevelState = { original: DEFAULT_LOG_LEVEL, raised: false }; + const pcap: CaptureState = { started: false }; + const opts: BundleOptions = { + trace, + capture, + capturePackets, + hasWindow: capture && totalSec > 0, + totalSec, + uploadUrl: upload ? NETBIRD_UPLOAD_URL : "", + anonymize, + systemInfo, + }; + + try { + await runBundleFlow(signal, opts, level, pcap, setStage, setLastBundlePath); + } catch (e) { + if (isAbort(e)) { + setStage({ kind: "cancelling" }); + await cleanupBestEffort(pcap, level, true); + setStage({ kind: "idle" }); + return; + } + await cleanupBestEffort(pcap, level, false); + setStage({ kind: "idle" }); + await errorDialog({ + Title: i18next.t("settings.error.debugBundleTitle"), + Message: formatErrorMessage(e), + }); + } finally { + if (abortRef.current === ctrl) abortRef.current = null; + } + }; + + const openBundleDir = () => { + if (!lastBundlePath) return; + DebugSvc.RevealFile(lastBundlePath).catch((err: unknown) => + console.error("[DebugBundleContext] reveal failed", err), + ); + }; + + return { + anonymize, + setAnonymize, + systemInfo, + setSystemInfo, + upload, + setUpload, + trace, + setTrace, + capture, + setCapture, + traceMinutes, + setTraceMinutes, + capturePackets, + setCapturePackets, + stage, + isRunning, + lastBundlePath, + run, + cancel, + reset, + openBundleDir, + }; +}; + +export type DebugBundleContextValue = ReturnType; + +const DebugBundleContext = createContext(null); + +export const DebugBundleProvider = ({ children }: { children: ReactNode }) => { + const value = useDebugBundle(); + return {children}; +}; + +export const useDebugBundleContext = () => { + const ctx = useContext(DebugBundleContext); + if (!ctx) { + throw new Error("useDebugBundleContext must be used inside DebugBundleProvider"); + } + return ctx; +}; diff --git a/client/ui/frontend/src/contexts/DialogContext.tsx b/client/ui/frontend/src/contexts/DialogContext.tsx new file mode 100644 index 000000000..8a52e0dd0 --- /dev/null +++ b/client/ui/frontend/src/contexts/DialogContext.tsx @@ -0,0 +1,68 @@ +import { + createContext, + type ReactNode, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from "react"; +import { ConfirmModal } from "@/components/dialog/ConfirmModal"; + +export type ConfirmOptions = { + title: ReactNode; + description: ReactNode; + confirmLabel: string; + cancelLabel?: string; + danger?: boolean; +}; + +type DialogContextValue = { + confirm: (options: ConfirmOptions) => Promise; +}; + +const DialogContext = createContext(null); + +export function DialogProvider({ children }: Readonly<{ children: ReactNode }>) { + const [open, setOpen] = useState(false); + const [options, setOptions] = useState(null); + const resolverRef = useRef<((result: boolean) => void) | null>(null); + + const confirm = useCallback((opts: ConfirmOptions) => { + setOptions(opts); + setOpen(true); + return new Promise((resolve) => { + resolverRef.current = resolve; + }); + }, []); + + const settle = (result: boolean) => { + resolverRef.current?.(result); + resolverRef.current = null; + setOpen(false); + }; + + const value = useMemo(() => ({ confirm }), [confirm]); + + return ( + + {children} + settle(true)} + onCancel={() => settle(false)} + /> + + ); +} + +export const useConfirm = () => { + const ctx = useContext(DialogContext); + if (!ctx) throw new Error("useConfirm must be used within a DialogProvider"); + return ctx.confirm; +}; diff --git a/client/ui/frontend/src/contexts/NavSectionContext.tsx b/client/ui/frontend/src/contexts/NavSectionContext.tsx new file mode 100644 index 000000000..c08a3c824 --- /dev/null +++ b/client/ui/frontend/src/contexts/NavSectionContext.tsx @@ -0,0 +1,24 @@ +import { createContext, useContext, useMemo, useState, type ReactNode } from "react"; + +export type NavSection = "peers" | "networks"; + +type NavSectionContextValue = { + section: NavSection; + setSection: (s: NavSection) => void; +}; + +const NavSectionContext = createContext(null); + +export const useNavSection = (): NavSectionContextValue => { + const ctx = useContext(NavSectionContext); + if (!ctx) { + throw new Error("useNavSection must be used inside NavSectionProvider"); + } + return ctx; +}; + +export const NavSectionProvider = ({ children }: { children: ReactNode }) => { + const [section, setSection] = useState("peers"); + const value = useMemo(() => ({ section, setSection }), [section]); + return {children}; +}; diff --git a/client/ui/frontend/src/contexts/NetworksContext.tsx b/client/ui/frontend/src/contexts/NetworksContext.tsx new file mode 100644 index 000000000..ef7231700 --- /dev/null +++ b/client/ui/frontend/src/contexts/NetworksContext.tsx @@ -0,0 +1,222 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { Networks as NetworksSvc } from "@bindings/services"; +import type { Network } from "@bindings/services/models.js"; +import { useStatus } from "@/contexts/StatusContext"; + +// A route that covers all traffic (0.0.0.0/0 or ::/0) is an exit node. +// The daemon may merge a v4+v6 pair into a single comma-joined range string. +export const isExitNode = (range: string): boolean => + range.split(",").some((part) => { + const trimmed = part.trim(); + return trimmed === "0.0.0.0/0" || trimmed === "::/0"; + }); + +type NetworksContextValue = { + routes: Network[]; + networkRoutes: Network[]; + exitNodes: Network[]; + activeExitNode: Network | null; + refresh: () => Promise; + toggleNetwork: (id: string, selected: boolean) => Promise; + toggleExitNode: (id: string, selected: boolean) => Promise; + setNetworksSelected: (ids: string[], selected: boolean) => Promise; +}; + +const NetworksContext = createContext(null); + +export const useNetworks = () => { + const ctx = useContext(NetworksContext); + if (!ctx) { + throw new Error("useNetworks must be used inside NetworksProvider"); + } + return ctx; +}; + +export const NetworksProvider = ({ children }: { children: ReactNode }) => { + const { status } = useStatus(); + const [routes, setRoutes] = useState([]); + const [pending, setPending] = useState>(new Map()); + const pendingRef = useRef(pending); + useEffect(() => { + pendingRef.current = pending; + }, [pending]); + + // Safety timer: if a prediction diverges from the daemon, the override would mask the true value forever. + const STUCK_OVERRIDE_MS = 4000; + const timersRef = useRef>>(new Map()); + + const clearTimer = useCallback((id: string) => { + const tid = timersRef.current.get(id); + if (tid !== undefined) { + clearTimeout(tid); + timersRef.current.delete(id); + } + }, []); + + const clearPendingFor = useCallback( + (ids: string[]) => { + for (const id of ids) clearTimer(id); + setPending((prev) => { + if (ids.every((id) => !prev.has(id))) return prev; + const next = new Map(prev); + for (const id of ids) next.delete(id); + return next; + }); + }, + [clearTimer], + ); + + const setPendingFor = useCallback( + (updates: Array<[string, boolean]>) => { + setPending((prev) => { + const next = new Map(prev); + for (const [id, sel] of updates) next.set(id, sel); + return next; + }); + for (const [id] of updates) { + clearTimer(id); + timersRef.current.set( + id, + setTimeout(() => clearPendingFor([id]), STUCK_OVERRIDE_MS), + ); + } + }, + [clearTimer, clearPendingFor], + ); + + useEffect(() => { + const timers = timersRef.current; + return () => { + for (const tid of timers.values()) clearTimeout(tid); + timers.clear(); + }; + }, []); + + const refresh = useCallback(async () => { + try { + const list = await NetworksSvc.List(); + setRoutes(list); + } catch (e) { + console.error("[NetworksContext] refresh failed", e); + } + }, []); + + const networksRevision = status?.networksRevision; + useEffect(() => { + refresh().catch((err: unknown) => console.error("[NetworksContext] refresh failed", err)); + }, [refresh, networksRevision]); + + useEffect(() => { + if (pendingRef.current.size === 0) return; + const confirmed: string[] = []; + for (const r of routes) { + const expected = pendingRef.current.get(r.id); + if (expected !== undefined && r.selected === expected) { + confirmed.push(r.id); + } + } + if (confirmed.length > 0) clearPendingFor(confirmed); + }, [routes, clearPendingFor]); + + const mutate = useCallback( + async (ids: string[], selected: boolean, rollback: Array<[string, boolean]>) => { + try { + if (selected) { + await NetworksSvc.Select({ networkIds: ids, append: true, all: false }); + } else { + await NetworksSvc.Deselect({ networkIds: ids, append: false, all: false }); + } + // Don't clear pending here — let the snapshot-match effect confirm, else a refresh racing the RPC return flashes back. + await refresh(); + } catch (e) { + console.error(e); + setPending((prev) => { + const next = new Map(prev); + for (const [id] of rollback) next.delete(id); + return next; + }); + throw e; + } + }, + [refresh], + ); + + const toggleNetwork = useCallback( + async (id: string, selected: boolean) => { + const target = !selected; + setPendingFor([[id, target]]); + await mutate([id], target, [[id, selected]]).catch(() => {}); + }, + [mutate, setPendingFor], + ); + + const setNetworksSelected = useCallback( + async (ids: string[], selected: boolean) => { + if (ids.length === 0) return; + const prevById = new Map(routes.map((r) => [r.id, r.selected])); + const rollback: Array<[string, boolean]> = ids.map((id) => [ + id, + prevById.get(id) ?? !selected, + ]); + setPendingFor(ids.map((id) => [id, selected])); + await mutate(ids, selected, rollback).catch(() => {}); + }, + [mutate, setPendingFor, routes], + ); + + // Daemon enforces exit-node mutual exclusion; mirror it locally so the optimistic paint matches. + const toggleExitNode = useCallback( + async (id: string, selected: boolean) => { + const target = !selected; + const updates: Array<[string, boolean]> = [[id, target]]; + const rollback: Array<[string, boolean]> = [[id, selected]]; + if (target) { + for (const r of routes) { + if (r.id !== id && isExitNode(r.range) && r.selected) { + updates.push([r.id, false]); + rollback.push([r.id, true]); + } + } + } + setPendingFor(updates); + await mutate([id], target, rollback).catch(() => {}); + }, + [mutate, setPendingFor, routes], + ); + + const value = useMemo(() => { + const effective = + pending.size === 0 + ? routes + : routes.map((r) => { + const override = pending.get(r.id); + return override === undefined || override === r.selected + ? r + : { ...r, selected: override }; + }); + const networkRoutes = effective.filter((r) => !isExitNode(r.range)); + const exitNodes = effective.filter((r) => isExitNode(r.range)); + const activeExitNode = exitNodes.find((r) => r.selected) ?? null; + return { + routes: effective, + networkRoutes, + exitNodes, + activeExitNode, + refresh, + toggleNetwork, + toggleExitNode, + setNetworksSelected, + }; + }, [routes, pending, refresh, toggleNetwork, toggleExitNode, setNetworksSelected]); + + return {children}; +}; diff --git a/client/ui/frontend/src/contexts/PeerDetailContext.tsx b/client/ui/frontend/src/contexts/PeerDetailContext.tsx new file mode 100644 index 000000000..3ab20891f --- /dev/null +++ b/client/ui/frontend/src/contexts/PeerDetailContext.tsx @@ -0,0 +1,50 @@ +import { + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import type { PeerStatus } from "@bindings/services/models.js"; + +type PeerDetailContextValue = { + selected: PeerStatus | null; + setSelected: (p: PeerStatus | null) => void; +}; + +const PeerDetailContext = createContext(null); + +export const usePeerDetail = (): PeerDetailContextValue => { + const ctx = useContext(PeerDetailContext); + if (!ctx) { + throw new Error("usePeerDetail must be used inside PeerDetailProvider"); + } + return ctx; +}; + +export const PeerDetailProvider = ({ children }: { children: ReactNode }) => { + const [selected, setSelected] = useState(null); + const openerRef = useRef(null); + + const select = useCallback((p: PeerStatus | null) => { + if (p) { + const active = document.activeElement; + openerRef.current = active instanceof HTMLElement ? active : null; + } else { + const opener = openerRef.current; + openerRef.current = null; + if (opener?.isConnected) { + queueMicrotask(() => opener.focus()); + } + } + setSelected(p); + }, []); + + const value = useMemo( + () => ({ selected, setSelected: select }), + [selected, select], + ); + return {children}; +}; diff --git a/client/ui/frontend/src/contexts/ProfileContext.tsx b/client/ui/frontend/src/contexts/ProfileContext.tsx new file mode 100644 index 000000000..4dd3eaa7a --- /dev/null +++ b/client/ui/frontend/src/contexts/ProfileContext.tsx @@ -0,0 +1,182 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { Events } from "@wailsio/runtime"; +import { Connection, ProfileSwitcher, Profiles as ProfilesSvc } from "@bindings/services"; +import type { Profile } from "@bindings/services/models.js"; +import i18next from "@/lib/i18n"; +import { errorDialog, formatErrorMessage } from "@/lib/errors"; + +const EVENT_PROFILE_CHANGED = "netbird:profile:changed"; + +type ProfileContextValue = { + username: string; + // activeProfile is the display NAME of the active profile (for rendering + // and the "default" check). activeProfileId is its stable on-disk ID, used + // as the handle for daemon requests and for active-profile comparisons, + // since display names can collide. + activeProfile: string; + activeProfileId: string; + profiles: Profile[]; + loaded: boolean; + refresh: () => Promise; + switchProfile: (id: string) => Promise; + addProfile: (name: string) => Promise; + removeProfile: (id: string) => Promise; + renameProfile: (id: string, newName: string) => Promise; + logoutProfile: (id: string) => Promise; +}; + +const ProfileContext = createContext(null); + +export const useProfile = () => { + const ctx = useContext(ProfileContext); + if (!ctx) { + throw new Error("useProfile must be used inside ProfileProvider"); + } + return ctx; +}; + +export const ProfileProvider = ({ children }: { children: ReactNode }) => { + const [username, setUsername] = useState(""); + const [activeProfile, setActiveProfile] = useState(""); + const [activeProfileId, setActiveProfileId] = useState(""); + const [profiles, setProfiles] = useState([]); + const [loaded, setLoaded] = useState(false); + const retryRef = useRef | null>(null); + + const refresh = useCallback(async () => { + if (retryRef.current) { + clearTimeout(retryRef.current); + retryRef.current = null; + } + try { + const u = await ProfilesSvc.Username(); + const [active, list] = await Promise.all([ + ProfilesSvc.GetActive(), + ProfilesSvc.List(u), + ]); + setUsername(u); + setActiveProfile(active.profileName || "default"); + setActiveProfileId(active.id || "default"); + setProfiles(list); + setLoaded(true); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + if (msg.includes("code = Unavailable")) { + retryRef.current = setTimeout(() => { + void refresh(); + }, 1000); + return; + } + setLoaded(true); + await errorDialog({ + Title: i18next.t("profile.error.loadTitle"), + Message: formatErrorMessage(e), + }); + } + }, []); + + useEffect(() => { + refresh().catch((err: unknown) => console.error("[ProfileContext] refresh failed", err)); + return () => { + if (retryRef.current) clearTimeout(retryRef.current); + }; + }, [refresh]); + + useEffect(() => { + const off = Events.On(EVENT_PROFILE_CHANGED, () => { + refresh().catch((err: unknown) => + console.error("[ProfileContext] refresh failed", err), + ); + }); + return () => { + off(); + }; + }, [refresh]); + + // id is a handle: the daemon resolves an exact ID, ID prefix, or unique + // display name. The UI passes the profile's ID for precision. + const switchProfile = useCallback( + async (id: string) => { + await ProfileSwitcher.SwitchActive({ profileName: id, username }); + await refresh(); + }, + [username, refresh], + ); + + // addProfile creates a profile by display name and returns the + // daemon-generated ID, so the caller can immediately address it by ID. + const addProfile = useCallback( + async (name: string) => { + const id = await ProfilesSvc.Add({ profileName: name, username }); + await refresh(); + return id; + }, + [username, refresh], + ); + + const removeProfile = useCallback( + async (id: string) => { + await ProfilesSvc.Remove({ profileName: id, username }); + await refresh(); + }, + [username, refresh], + ); + + // The daemon resolves the handle (exact ID, ID prefix, or unique display + // name) — passing the ID is precise and avoids collisions on rename. + const renameProfile = useCallback( + async (id: string, newName: string) => { + await ProfilesSvc.Rename({ handle: id, newName, username }); + await refresh(); + }, + [username, refresh], + ); + + const logoutProfile = useCallback( + async (id: string) => { + await Connection.Logout({ profileName: id, username }); + await refresh(); + }, + [username, refresh], + ); + + const value = useMemo( + () => ({ + username, + activeProfile, + activeProfileId, + profiles, + loaded, + refresh, + switchProfile, + addProfile, + removeProfile, + renameProfile, + logoutProfile, + }), + [ + username, + activeProfile, + activeProfileId, + profiles, + loaded, + refresh, + switchProfile, + addProfile, + removeProfile, + renameProfile, + logoutProfile, + ], + ); + + return {children}; +}; diff --git a/client/ui/frontend/src/contexts/RestrictionsContext.tsx b/client/ui/frontend/src/contexts/RestrictionsContext.tsx new file mode 100644 index 000000000..572bf17ec --- /dev/null +++ b/client/ui/frontend/src/contexts/RestrictionsContext.tsx @@ -0,0 +1,65 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import { Events } from "@wailsio/runtime"; +import { Settings as SettingsSvc } from "@bindings/services"; +import { Restrictions } from "@bindings/services/models.js"; +import { useStatus } from "@/contexts/StatusContext.tsx"; + +const EVENT_SYSTEM = "netbird:event"; +const EMPTY = new Restrictions(); + +const RestrictionsContext = createContext(EMPTY); + +export const useRestrictions = () => useContext(RestrictionsContext); + +export const RestrictionsProvider = ({ children }: { children: ReactNode }) => { + const [restrictions, setRestrictions] = useState(EMPTY); + const mounted = useRef(true); + const { status } = useStatus(); + + const refresh = useCallback(async () => { + try { + const r = await SettingsSvc.GetRestrictions(); + if (mounted.current) setRestrictions(r); + } catch (e) { + console.error("[RestrictionsContext] refresh failed", e); + } + }, []); + + useEffect(() => { + mounted.current = true; + + const off = Events.On( + EVENT_SYSTEM, + (e: { data?: { metadata?: { [k: string]: string | undefined } } }) => { + if (e.data?.metadata?.type === "config_changed") refresh(); + }, + ); + + const onVisible = () => { + if (document.visibilityState === "visible") refresh(); + }; + document.addEventListener("visibilitychange", onVisible); + + return () => { + mounted.current = false; + off(); + document.removeEventListener("visibilitychange", onVisible); + }; + }, [refresh]); + + useEffect(() => { + if (status?.status) refresh(); + }, [status?.status, refresh]); + + return ( + {children} + ); +}; diff --git a/client/ui/frontend/src/contexts/SettingsContext.tsx b/client/ui/frontend/src/contexts/SettingsContext.tsx new file mode 100644 index 000000000..37627bc76 --- /dev/null +++ b/client/ui/frontend/src/contexts/SettingsContext.tsx @@ -0,0 +1,267 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { Events } from "@wailsio/runtime"; +import { Autostart, Settings as SettingsSvc, Version } from "@bindings/services"; +import type { Config } from "@bindings/services/models.js"; +import i18next from "@/lib/i18n"; +import { useProfile } from "@/contexts/ProfileContext.tsx"; +import { SettingsSkeleton } from "@/modules/settings/SettingsSkeleton.tsx"; +import { errorDialog, formatErrorMessage as errorMessage } from "@/lib/errors.ts"; + +const SAVE_DEBOUNCE_MS = 400; + +const logSaveError = (err: unknown) => console.error("[SettingsContext] save failed", err); + +export type AutostartState = { supported: boolean; enabled: boolean }; + +type SettingsContextValue = { + config: Config; + guiVersion: string; + setField: (k: K, v: Config[K]) => void; + saveField: (k: K, v: Config[K]) => Promise; + saveFields: (partial: Partial, opts?: { preSharedKey?: string }) => Promise; + saveNow: () => Promise; +}; + +type AutostartContextValue = { + autostart: AutostartState | null; + setAutostartEnabled: (enabled: boolean) => Promise; +}; + +const SettingsContext = createContext(null); +const AutostartContext = createContext(null); + +export const useSettings = () => { + const ctx = useContext(SettingsContext); + if (!ctx) { + throw new Error("useSettings must be used inside SettingsProvider"); + } + return ctx; +}; + +export const useAutostartSetting = () => { + const ctx = useContext(AutostartContext); + if (!ctx) { + throw new Error("useAutostartSetting must be used inside AutostartSettingsProvider"); + } + return ctx; +}; + +type LoadedConfig = { profileName: string; data: Config }; + +const useSettingsState = () => { + const { username, activeProfileId, loaded: profileLoaded } = useProfile(); + const [loaded, setLoaded] = useState(null); + const [guiVersion, setGuiVersion] = useState("—"); + const saveTimer = useRef | null>(null); + const loadedRef = useRef(null); + + useEffect(() => { + loadedRef.current = loaded; + }, [loaded]); + + useEffect(() => { + if (!profileLoaded || !activeProfileId) return; + let cancelled = false; + + const load = async (showError: boolean) => { + try { + const data = await SettingsSvc.GetConfig({ + profileName: activeProfileId, + username, + }); + if (cancelled) return; + if (saveTimer.current) return; + setLoaded({ profileName: activeProfileId, data }); + } catch (e) { + if (cancelled || !showError) return; + await errorDialog({ + Title: i18next.t("settings.error.loadTitle"), + Message: errorMessage(e), + }); + } + }; + + load(true); + + const off = Events.On( + "netbird:event", + (e: { data?: { metadata?: { [k: string]: string | undefined } } }) => { + if (e.data?.metadata?.type === "config_changed") load(false); + }, + ); + + return () => { + cancelled = true; + off(); + }; + }, [profileLoaded, activeProfileId, username]); + + useEffect(() => { + let cancelled = false; + Version.GUI().then((v) => { + if (!cancelled) setGuiVersion(v); + }); + return () => { + cancelled = true; + }; + }, []); + + useEffect( + () => () => { + if (saveTimer.current) clearTimeout(saveTimer.current); + }, + [], + ); + + const save = useCallback( + async (profileName: string, next: Config, preSharedKey?: string) => { + const preSharedKeyWrite = preSharedKey === undefined ? {} : { preSharedKey }; + try { + await SettingsSvc.SetConfig({ + ...next, + ...preSharedKeyWrite, + profileName, + username, + }); + } catch (e) { + await errorDialog({ + Title: i18next.t("settings.error.saveTitle"), + Message: errorMessage(e), + }); + } + }, + [username], + ); + + const setField = useCallback( + (k: K, v: Config[K]) => { + const cur = loadedRef.current; + if (!cur) return; + const next: LoadedConfig = { + profileName: cur.profileName, + data: { ...cur.data, [k]: v }, + }; + loadedRef.current = next; + setLoaded(next); + if (saveTimer.current) clearTimeout(saveTimer.current); + saveTimer.current = setTimeout(() => { + saveTimer.current = null; + save(next.profileName, next.data).catch(logSaveError); + }, SAVE_DEBOUNCE_MS); + }, + [save], + ); + + const saveNow = useCallback(async () => { + if (!loaded) return; + if (saveTimer.current) { + clearTimeout(saveTimer.current); + saveTimer.current = null; + } + await save(loaded.profileName, loaded.data); + }, [loaded, save]); + + const saveField = useCallback( + async (k: K, v: Config[K]) => { + if (!loaded) return; + if (saveTimer.current) { + clearTimeout(saveTimer.current); + saveTimer.current = null; + } + const next = { ...loaded.data, [k]: v }; + setLoaded({ profileName: loaded.profileName, data: next }); + await save(loaded.profileName, next); + }, + [loaded, save], + ); + + const saveFields = useCallback( + async (partial: Partial, opts?: { preSharedKey?: string }) => { + if (!loaded) return; + if (saveTimer.current) { + clearTimeout(saveTimer.current); + saveTimer.current = null; + } + + const merged: Config = { ...loaded.data, ...partial }; + const next: Config = + opts?.preSharedKey === undefined + ? merged + : { ...merged, preSharedKeySet: opts.preSharedKey !== "" }; + setLoaded({ profileName: loaded.profileName, data: next }); + await save(loaded.profileName, next, opts?.preSharedKey); + }, + [loaded, save], + ); + + return { config: loaded?.data ?? null, guiVersion, setField, saveField, saveFields, saveNow }; +}; + +export const SettingsProvider = ({ children }: { children: ReactNode }) => { + const { config, guiVersion, setField, saveField, saveFields, saveNow } = useSettingsState(); + + const value = useMemo( + () => (config ? { config, guiVersion, setField, saveField, saveFields, saveNow } : null), + [config, guiVersion, setField, saveField, saveFields, saveNow], + ); + + if (!value) { + return ( +
+ +
+ ); + } + + return {children}; +}; + +export const AutostartSettingsProvider = ({ children }: { children: ReactNode }) => { + const [autostart, setAutostart] = useState(null); + + useEffect(() => { + let cancelled = false; + (async () => { + const supported = await Autostart.Supported(); + const enabled = supported ? await Autostart.IsEnabled() : false; + if (cancelled) return; + setAutostart({ supported, enabled }); + })().catch((err: unknown) => { + if (cancelled) return; + console.warn("[SettingsContext] load autostart state failed", err); + setAutostart({ supported: false, enabled: false }); + }); + return () => { + cancelled = true; + }; + }, []); + + const setAutostartEnabled = useCallback(async (enabled: boolean) => { + setAutostart((s) => (s ? { ...s, enabled } : s)); + try { + await Autostart.SetEnabled(enabled); + } catch (e) { + setAutostart((s) => (s ? { ...s, enabled: !enabled } : s)); + await errorDialog({ + Title: i18next.t("settings.general.autostart.errorTitle"), + Message: errorMessage(e), + }); + } + }, []); + + const value = useMemo( + () => ({ autostart, setAutostartEnabled }), + [autostart, setAutostartEnabled], + ); + + return {children}; +}; diff --git a/client/ui/frontend/src/contexts/StatusContext.tsx b/client/ui/frontend/src/contexts/StatusContext.tsx new file mode 100644 index 000000000..0ad9c7875 --- /dev/null +++ b/client/ui/frontend/src/contexts/StatusContext.tsx @@ -0,0 +1,106 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; +import { Events } from "@wailsio/runtime"; +import { DaemonFeed } from "@bindings/services"; +import { Status } from "@bindings/services/models.js"; +import { DaemonOutdatedOverlay } from "@/components/empty-state/DaemonOutdatedOverlay.tsx"; +import { DaemonUnavailableOverlay } from "@/components/empty-state/DaemonUnavailableOverlay.tsx"; +import { isDaemonCompatible } from "@/lib/compat"; + +const EVENT_STATUS = "netbird:status"; + +type StatusContextValue = { + status: Status | null; + error: string | null; + refresh: () => Promise; + isReady: boolean; + isDaemonUnavailable: boolean; + isDaemonAvailable: boolean; + isDaemonOutdated: boolean; +}; + +const StatusContext = createContext(null); + +export const useStatus = () => { + const ctx = useContext(StatusContext); + if (!ctx) { + throw new Error("useStatus must be used inside StatusProvider"); + } + return ctx; +}; + +export const StatusProvider = ({ children }: { children: ReactNode }) => { + const [status, setStatus] = useState(null); + const [error, setError] = useState(null); + const [isDaemonOutdated, setIsDaemonOutdated] = useState(false); + + const refresh = useCallback(async () => { + try { + const s = await DaemonFeed.Get(); + setStatus(s); + setError(null); + } catch (e) { + // Synthesize DaemonUnavailable so cold-start-without-daemon isn't a blank UI (isReady stays false otherwise). + setStatus(Status.createFrom({ status: "DaemonUnavailable" })); + setError(String(e)); + } + }, []); + + useEffect(() => { + refresh().catch((err: unknown) => console.error("[StatusContext] refresh failed", err)); + const off = Events.On(EVENT_STATUS, (ev: { data: Status }) => { + setStatus(ev.data); + setError(null); + }); + return () => { + off(); + }; + }, [refresh]); + + const isReady = status !== null; + const isDaemonUnavailable = isReady && status.status === "DaemonUnavailable"; + const isDaemonAvailable = isReady && !isDaemonUnavailable; + + useEffect(() => { + if (!isDaemonAvailable) return; + let cancelled = false; + isDaemonCompatible() + .then((ok) => { + if (!cancelled) setIsDaemonOutdated(!ok); + }) + .catch((err) => { + console.error("[StatusContext] daemon compatible error", err); + }); + return () => { + cancelled = true; + }; + }, [isDaemonAvailable]); + + const value = useMemo( + () => ({ + status, + error, + refresh, + isReady, + isDaemonUnavailable, + isDaemonAvailable, + isDaemonOutdated, + }), + [status, error, refresh, isReady, isDaemonUnavailable, isDaemonAvailable, isDaemonOutdated], + ); + + return ( + + {isDaemonAvailable && !isDaemonOutdated && children} + + + + ); +}; diff --git a/client/ui/frontend/src/contexts/ViewModeContext.tsx b/client/ui/frontend/src/contexts/ViewModeContext.tsx new file mode 100644 index 000000000..7db3abae0 --- /dev/null +++ b/client/ui/frontend/src/contexts/ViewModeContext.tsx @@ -0,0 +1,89 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { Window } from "@wailsio/runtime"; +import { Preferences } from "@bindings/services"; +import { ViewMode as ViewModePref } from "@bindings/preferences/models.js"; + +export type ViewMode = "default" | "advanced"; + +// Don't pass a fixed height to Window.SetSize: macOS SetSize is frame (incl. ~28px +// title bar) while creation is content, so re-asserting a constant chops the content on first switch. +export const VIEW_WIDTH: Record = { + default: 380, + advanced: 900, +}; + +type ViewModeContextValue = { + viewMode: ViewMode; + setViewMode: (mode: ViewMode) => void; +}; + +const ViewModeContext = createContext(null); + +export const ViewModeProvider = ({ children }: { children: ReactNode }) => { + const [mode, setMode] = useState("default"); + const modeRef = useRef("default"); + + useEffect(() => { + let cancelled = false; + Preferences.Get() + .then((prefs) => { + if (cancelled) return; + const saved = prefs?.viewMode as ViewMode | undefined; + if (saved === "default" || saved === "advanced") { + modeRef.current = saved; + setMode(saved); + } + }) + .catch((err: unknown) => + console.warn("[ViewModeContext] load preferences failed", err), + ); + return () => { + cancelled = true; + }; + }, []); + + // Resize before flipping React state, else the layout paints into a window that hasn't grown yet. + const setViewMode = useCallback((mode: ViewMode) => { + if (modeRef.current === mode) return; + modeRef.current = mode; + (async () => { + const size = await Window.Size().catch((err: unknown) => { + console.warn("[ViewModeContext] read window size failed", err); + return null; + }); + const width = VIEW_WIDTH[mode]; + const height = size?.height ?? 640; + await Window.SetSize(width, height).catch((err: unknown) => + console.warn("[ViewModeContext] set window size failed", err), + ); + setMode(mode); + const pref = + mode === "advanced" ? ViewModePref.ViewModeAdvanced : ViewModePref.ViewModeDefault; + Preferences.SetViewMode(pref).catch((err: unknown) => + console.error("[ViewModeContext] SetViewMode failed", err), + ); + })().catch((err: unknown) => console.error("[ViewModeContext] setViewMode failed", err)); + }, []); + + const value = useMemo( + () => ({ viewMode: mode, setViewMode }), + [mode, setViewMode], + ); + + return {children}; +}; + +export const useViewMode = () => { + const ctx = useContext(ViewModeContext); + if (!ctx) throw new Error("useViewMode must be used inside ViewModeProvider"); + return ctx; +}; diff --git a/client/ui/frontend/src/globals.css b/client/ui/frontend/src/globals.css new file mode 100644 index 000000000..84ddfdcfe --- /dev/null +++ b/client/ui/frontend/src/globals.css @@ -0,0 +1,45 @@ +@font-face { + font-family: "Inter Variable"; + font-style: normal; + font-weight: 100 900; + src: url("./assets/fonts/inter-variable.ttf") format("truetype"); +} + +@font-face { + font-family: "JetBrains Mono Variable"; + font-style: normal; + font-weight: 100 800; + src: url("./assets/fonts/jetbrains-mono-variable.ttf") format("truetype"); +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +#root { + height: 100%; + overflow: hidden; +} + +/* + * Body bg is fully opaque on purpose. The main window uses + * MacBackdropTranslucent (main.go) and TitleBarHiddenInset, which on macOS + * lets the desktop wallpaper bleed through any non-opaque pixel. A 90% + * body alpha meant two machines with different wallpapers saw different + * effective backgrounds. Matching Wails' BackgroundColour (#181A1D / nb-gray + * DEFAULT) here keeps things consistent regardless of the OS backdrop. + */ +body { + @apply bg-nb-gray font-sans text-nb-gray-200 antialiased; +} + +.wails-draggable { + --wails-draggable: drag; + cursor: default; +} + +.wails-no-draggable { + --wails-draggable: no-drag; +} diff --git a/client/ui/frontend/src/hooks/useAutoSizeWindow.ts b/client/ui/frontend/src/hooks/useAutoSizeWindow.ts new file mode 100644 index 000000000..d4f4d80b2 --- /dev/null +++ b/client/ui/frontend/src/hooks/useAutoSizeWindow.ts @@ -0,0 +1,60 @@ +import { useLayoutEffect, useRef } from "react"; +import { Window } from "@wailsio/runtime"; +import i18next from "@/lib/i18n"; +import { isLinux } from "@/lib/platform"; + +// Sizes the current Wails window to the measured content height (keeping `width`), +// then shows it. Re-applies on content resize and language change. +export function useAutoSizeWindow(width: number, ready: boolean = true) { + const ref = useRef(null); + useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + let shown = false; + let raf1 = 0; + let raf2 = 0; + const showOnce = () => { + if (shown) return; + shown = true; + Window.Show().catch(() => {}); + Window.Focus().catch(() => {}); + }; + const apply = async () => { + if (!ready) return; + const h = Math.ceil(el.getBoundingClientRect().height); + if (h <= 0) return; + try { + // Window.SetSize takes the frame size, so add the OS title-bar height or content clips. + const frame = await Window.Size(); + const targetH = h + Math.max(0, frame.height - window.innerHeight); + // Linux: SetSize no-ops on a mapped non-resizable window (X11), so pin via min/max instead. + if (isLinux()) { + await Window.SetMinSize(width, targetH); + await Window.SetMaxSize(width, targetH); + } + await Window.SetSize(width, targetH); + showOnce(); + } catch { + // window gone / not ready — ignore + } + }; + const scheduleApply = () => { + cancelAnimationFrame(raf1); + cancelAnimationFrame(raf2); + raf1 = requestAnimationFrame(() => { + raf2 = requestAnimationFrame(apply); + }); + }; + apply(); + const ro = new ResizeObserver(apply); + ro.observe(el); + i18next.on("languageChanged", scheduleApply); + return () => { + ro.disconnect(); + cancelAnimationFrame(raf1); + cancelAnimationFrame(raf2); + i18next.off("languageChanged", scheduleApply); + }; + }, [width, ready]); + return ref; +} diff --git a/client/ui/frontend/src/hooks/useFocusVisible.ts b/client/ui/frontend/src/hooks/useFocusVisible.ts new file mode 100644 index 000000000..6061beb9c --- /dev/null +++ b/client/ui/frontend/src/hooks/useFocusVisible.ts @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; + +// Tracks the user's current input modality (keyboard vs pointer) at module +// scope, mirroring what @react-aria/interactions does. Radix programmatically +// focuses elements like Tabs triggers and Select triggers, which makes the +// browser's :focus-visible heuristic light up on mouse-driven interactions too. +// Gating focus styles on this hook lets us only paint a focus ring when the +// user is actually navigating with the keyboard. +// See react-aria's useFocusVisible for context. + +type Modality = "keyboard" | "pointer"; + +let currentModality: Modality = "pointer"; +const subscribers = new Set<(m: Modality) => void>(); + +const setModality = (m: Modality) => { + if (m === currentModality) return; + currentModality = m; + subscribers.forEach((cb) => cb(m)); +}; + +const isKeyboardEvent = (e: KeyboardEvent) => { + if (e.metaKey || e.ctrlKey || e.altKey) return false; + return e.key === "Tab" || e.key === "Escape" || e.key.startsWith("Arrow"); +}; + +if (globalThis.window !== undefined) { + globalThis.addEventListener( + "keydown", + (e) => { + if (isKeyboardEvent(e)) setModality("keyboard"); + }, + true, + ); + globalThis.addEventListener("pointerdown", () => setModality("pointer"), true); +} + +export const useFocusVisible = (): boolean => { + const [visible, setVisible] = useState(currentModality === "keyboard"); + useEffect(() => { + setVisible(currentModality === "keyboard"); + const cb = (m: Modality) => setVisible(m === "keyboard"); + subscribers.add(cb); + return () => { + subscribers.delete(cb); + }; + }, []); + return visible; +}; diff --git a/client/ui/frontend/src/hooks/useKeyboardShortcut.ts b/client/ui/frontend/src/hooks/useKeyboardShortcut.ts new file mode 100644 index 000000000..ea67d8375 --- /dev/null +++ b/client/ui/frontend/src/hooks/useKeyboardShortcut.ts @@ -0,0 +1,46 @@ +import { useEffect } from "react"; +import { isMacOS } from "@/lib/platform"; + +export type Shortcut = { + key: string; + cmd?: boolean; + shift?: boolean; + alt?: boolean; + preventDefault?: boolean; +}; + +export const useKeyboardShortcut = (shortcut: Shortcut, callback: () => void, enabled = true) => { + useEffect(() => { + if (!enabled) return; + const onKey = (e: KeyboardEvent) => { + if (e.key.toLowerCase() !== shortcut.key.toLowerCase()) return; + const mod = e.metaKey || e.ctrlKey; + if (!!shortcut.cmd !== mod) return; + if (!!shortcut.shift !== e.shiftKey) return; + if (!!shortcut.alt !== e.altKey) return; + if (shortcut.preventDefault !== false) e.preventDefault(); + callback(); + }; + globalThis.addEventListener("keydown", onKey); + return () => globalThis.removeEventListener("keydown", onKey); + }, [ + shortcut.key, + shortcut.cmd, + shortcut.shift, + shortcut.alt, + shortcut.preventDefault, + callback, + enabled, + ]); +}; + +export const formatShortcut = (shortcut: Shortcut): string => { + // navigator.platform is empty on some WebView2 builds → misrenders ⌘ as Ctrl on Mac. + const mac = isMacOS(); + const parts: string[] = []; + if (shortcut.cmd) parts.push(mac ? "⌘" : "Ctrl"); + if (shortcut.shift) parts.push(mac ? "⇧" : "Shift"); + if (shortcut.alt) parts.push(mac ? "⌥" : "Alt"); + parts.push(shortcut.key.length === 1 ? shortcut.key.toUpperCase() : shortcut.key); + return parts.join(mac ? "" : "+"); +}; diff --git a/client/ui/frontend/src/hooks/useManagementUrl.ts b/client/ui/frontend/src/hooks/useManagementUrl.ts new file mode 100644 index 000000000..6a0ef1b81 --- /dev/null +++ b/client/ui/frontend/src/hooks/useManagementUrl.ts @@ -0,0 +1,143 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSettings } from "@/contexts/SettingsContext.tsx"; +import { useConfirm } from "@/contexts/DialogContext.tsx"; + +export const CLOUD_MANAGEMENT_URL = "https://api.netbird.io:443"; +const CLOUD_MANAGEMENT_URLS = new Set([ + CLOUD_MANAGEMENT_URL, + "https://api.wiretrustee.com:443", // legacy cloud endpoint +]); + +export function isNetbirdCloud(url: string): boolean { + if (!url || url.trim() === "") return true; + return CLOUD_MANAGEMENT_URLS.has(url); +} + +// Matches http(s)://host[:port][/path][?query][#fragment]; host = domain, localhost, or IPv4. +// Syntactic validation only — reachability is checked via checkManagementUrlReachable. +export const URL_PATTERN = new RegExp( + String.raw`^(https?:\/\/)?` + + String.raw`((([a-z\d]([a-z\d-]*[a-z\d])?)\.)+[a-z]{2,}|localhost|` + + String.raw`((\d{1,3}\.){3}\d{1,3}))` + + String.raw`(\:\d+)?(\/[-a-z\d%_.~+]*)*` + + String.raw`(\?[;&a-z\d%_.~+=-]*)?` + + String.raw`(\#[-a-z\d_]*)?$`, + "i", +); + +export function normalizeManagementUrl(input: string): string { + const trimmed = input.trim(); + if (!trimmed) return ""; + if (/^https?:\/\//i.test(trimmed)) return trimmed; + return `https://${trimmed}`; +} + +export function isValidManagementUrl(input: string): boolean { + const trimmed = input.trim(); + if (!trimmed) return false; + return URL_PATTERN.test(trimmed); +} + +// Can false-negative for self-hosted behind internal DNS / self-signed certs — treat as a soft warning, not a hard block. +export async function checkManagementUrlReachable( + url: string, + timeoutMs: number = 5000, +): Promise { + const target = normalizeManagementUrl(url); + if (!target) return false; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + await fetch(target, { method: "GET", mode: "no-cors", signal: controller.signal }); + return true; + } catch { + return false; + } finally { + clearTimeout(timer); + } +} + +export enum ManagementMode { + Cloud = "cloud", + SelfHosted = "selfhosted", +} + +function modeFromUrl(url: string): ManagementMode { + return isNetbirdCloud(url) ? ManagementMode.Cloud : ManagementMode.SelfHosted; +} + +export function useManagementUrl() { + const { t } = useTranslation(); + const confirm = useConfirm(); + const { config, saveField } = useSettings(); + const [modeState, setModeState] = useState(modeFromUrl(config.managementUrl)); + const [url, setUrl] = useState( + isNetbirdCloud(config.managementUrl) ? "" : config.managementUrl, + ); + const [checking, setChecking] = useState(false); + const [unreachable, setUnreachable] = useState(false); + + useEffect(() => { + setModeState(modeFromUrl(config.managementUrl)); + if (!isNetbirdCloud(config.managementUrl)) { + setUrl(config.managementUrl); + } + }, [config.managementUrl]); + + useEffect(() => { + setUnreachable(false); + }, [url, modeState]); + + const setMode = async (next: ManagementMode) => { + if (next === ManagementMode.Cloud && !isNetbirdCloud(config.managementUrl)) { + const ok = await confirm({ + title: t("settings.general.management.switchCloudTitle"), + description: t("settings.general.management.switchCloudMessage"), + confirmLabel: t("settings.general.management.switchCloudConfirm"), + }); + if (!ok) return; + setModeState(ManagementMode.Cloud); + saveField("managementUrl", CLOUD_MANAGEMENT_URL).catch((err: unknown) => + console.error("save managementUrl failed", err), + ); + return; + } + setModeState(next); + }; + + const normalizedUrl = normalizeManagementUrl(url); + const urlValid = isValidManagementUrl(url); + const targetUrl = modeState === ManagementMode.Cloud ? CLOUD_MANAGEMENT_URL : normalizedUrl; + const dirty = targetUrl !== config.managementUrl; + const showError = modeState === ManagementMode.SelfHosted && url.trim() !== "" && !urlValid; + const canSave = dirty && (modeState === ManagementMode.Cloud || urlValid); + const displayUrl = modeState === ManagementMode.Cloud ? CLOUD_MANAGEMENT_URL : url; + + const save = async () => { + if (modeState === ManagementMode.SelfHosted && !unreachable) { + setChecking(true); + const reachable = await checkManagementUrlReachable(targetUrl); + setChecking(false); + if (!reachable) { + setUnreachable(true); + return; + } + } + await saveField("managementUrl", targetUrl); + setUnreachable(false); + }; + + return { + mode: modeState, + setMode, + url, + setUrl, + displayUrl, + showError, + canSave, + save, + checking, + unreachable, + }; +} diff --git a/client/ui/frontend/src/layouts/AppLayout.tsx b/client/ui/frontend/src/layouts/AppLayout.tsx new file mode 100644 index 000000000..1588d9d08 --- /dev/null +++ b/client/ui/frontend/src/layouts/AppLayout.tsx @@ -0,0 +1,27 @@ +import { Outlet } from "react-router-dom"; +import { ClientVersionProvider } from "@/contexts/ClientVersionContext.tsx"; +import { StatusProvider } from "@/contexts/StatusContext.tsx"; +import { DebugBundleProvider } from "@/contexts/DebugBundleContext.tsx"; +import { ProfileProvider } from "@/contexts/ProfileContext.tsx"; +import { DialogProvider } from "@/contexts/DialogContext.tsx"; +import { RestrictionsProvider } from "@/contexts/RestrictionsContext.tsx"; + +export const AppLayout = () => { + return ( +
+ + + + + + + + + + + + + +
+ ); +}; diff --git a/client/ui/frontend/src/layouts/AppRightPanel.tsx b/client/ui/frontend/src/layouts/AppRightPanel.tsx new file mode 100644 index 000000000..8474bd71f --- /dev/null +++ b/client/ui/frontend/src/layouts/AppRightPanel.tsx @@ -0,0 +1,38 @@ +import { type ReactNode } from "react"; +import { motion } from "framer-motion"; +import { cn } from "@/lib/cn.ts"; + +type Props = { + children: ReactNode; + overlay?: ReactNode; + overlayOpen?: boolean; + className?: string; +}; + +const PANEL_TRANSITION = { + duration: 0.32, + ease: [0.32, 0.72, 0, 1] as [number, number, number, number], +}; + +export const AppRightPanel = ({ children, overlay, overlayOpen = false, className }: Props) => { + return ( +
+ + {children} + + {overlay} +
+ ); +}; diff --git a/client/ui/frontend/src/lib/cn.ts b/client/ui/frontend/src/lib/cn.ts new file mode 100644 index 000000000..e6a8be071 --- /dev/null +++ b/client/ui/frontend/src/lib/cn.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/client/ui/frontend/src/lib/compat.ts b/client/ui/frontend/src/lib/compat.ts new file mode 100644 index 000000000..b2981da90 --- /dev/null +++ b/client/ui/frontend/src/lib/compat.ts @@ -0,0 +1,19 @@ +import { Compat } from "@bindings/services"; + +let cached: boolean | null = null; + +/** + * isDaemonCompatible probes whether the running daemon implements the WailsUIReady + * RPC. A false result means the daemon predates this UI (Unimplemented) and is too + * old to drive it. The Go side returns an error instead when the daemon is simply + * unreachable, so a throw here is NOT an outdated daemon — treat it as "unknown" + * and let the normal connection flow report it. + * + * The result is cached for the session: daemon identity does not change without a + * UI restart, and a freshly started daemon is reachable again under the same socket. + */ +export async function isDaemonCompatible(): Promise { + if (cached !== null) return cached; + cached = await Compat.DaemonReady(); + return cached; +} diff --git a/client/ui/frontend/src/lib/connection.ts b/client/ui/frontend/src/lib/connection.ts new file mode 100644 index 000000000..b9e98bf24 --- /dev/null +++ b/client/ui/frontend/src/lib/connection.ts @@ -0,0 +1,121 @@ +import { Events } from "@wailsio/runtime"; +import { Connection, WindowManager } from "@bindings/services"; +import i18next from "@/lib/i18n"; +import { errorDialog, formatErrorMessage } from "@/lib/errors.ts"; + +export const EVENT_BROWSER_LOGIN_CANCEL = "browser-login:cancel"; +export const EVENT_TRIGGER_LOGIN = "trigger-login"; + +let connectionInFlight = false; + +type SsoState = { + cancelled: boolean; + offCancel?: () => void; + offSignal?: () => void; +}; + +async function openBrowserLoginUri(uri: string): Promise { + try { + await WindowManager.OpenBrowserLogin(uri); + } catch (e) { + console.error(e); + } +} + +function buildSsoCancelPromise(state: SsoState, signal?: AbortSignal): Promise { + return new Promise((resolve) => { + state.offCancel = Events.On(EVENT_BROWSER_LOGIN_CANCEL, () => { + state.cancelled = true; + resolve(); + }); + if (!signal) return; + const onAbort = () => { + state.cancelled = true; + resolve(); + }; + if (signal.aborted) { + onAbort(); + return; + } + signal.addEventListener("abort", onAbort); + state.offSignal = () => signal.removeEventListener("abort", onAbort); + }); +} + +async function runSsoLogin( + result: { verificationUri: string; verificationUriComplete: string; userCode: string }, + state: SsoState, + signal?: AbortSignal, +): Promise { + const uri = result.verificationUriComplete || result.verificationUri; + if (uri) await openBrowserLoginUri(uri); + + const cancelPromise = buildSsoCancelPromise(state, signal); + const waitPromise = Connection.WaitSSOLogin({ userCode: result.userCode, hostname: "" }); + + try { + await Promise.race([waitPromise, cancelPromise]); + } finally { + WindowManager.CloseBrowserLogin().catch(console.error); + } + + if (state.cancelled) { + waitPromise.cancel?.(); + waitPromise.catch(() => {}); + } +} + +export async function startConnection(onSettled?: () => void, signal?: AbortSignal): Promise { + if (connectionInFlight || signal?.aborted) { + onSettled?.(); + return; + } + connectionInFlight = true; + + const state: SsoState = { cancelled: false }; + let connectError: unknown; + + try { + const result = await Connection.Login({ + profileName: "", + username: "", + managementUrl: "", + setupKey: "", + preSharedKey: "", + hostname: "", + hint: "", + }); + + if (signal?.aborted) state.cancelled = true; + + if (!state.cancelled && result.needsSsoLogin) { + await runSsoLogin(result, state, signal); + } + + if (!state.cancelled && signal?.aborted) state.cancelled = true; + + if (!state.cancelled) { + await Connection.Up({ profileName: "", username: "" }); + } + } catch (e) { + WindowManager.CloseBrowserLogin().catch(console.error); + if (!state.cancelled) connectError = e; + } finally { + state.offCancel?.(); + state.offSignal?.(); + connectionInFlight = false; + onSettled?.(); + } + + if (connectError !== undefined) { + await errorDialog({ + Title: i18next.t("connect.error.loginTitle"), + Message: formatErrorMessage(connectError), + }); + return; + } + + if (state.cancelled && signal) { + throw new DOMException("aborted", "AbortError"); + } +} diff --git a/client/ui/frontend/src/lib/errors.ts b/client/ui/frontend/src/lib/errors.ts new file mode 100644 index 000000000..34a90dace --- /dev/null +++ b/client/ui/frontend/src/lib/errors.ts @@ -0,0 +1,60 @@ +import { WindowManager } from "@bindings/services"; + +type ClassifiedError = { short: string; long: string }; + +const asObject = (v: unknown): Record | null => + v && typeof v === "object" ? (v as Record) : null; + +const parseJsonObject = (s: unknown): Record | null => { + if (typeof s !== "string") return null; + const t = s.trim(); + if (!t.startsWith("{") || !t.endsWith("}")) return null; + try { + return asObject(JSON.parse(t)); + } catch { + return null; + } +}; + +const toWailsEnvelope = (e: unknown): Record | null => { + const obj = asObject(e); + if (!obj) return null; + return asObject(obj.cause) ?? parseJsonObject(obj.message); +}; + +// Read { short, long } from wherever the classified error sits in the envelope +const toClassifiedError = (v: unknown): ClassifiedError | null => { + const o = asObject(v); + if (!o) return null; + const short = typeof o.short === "string" ? o.short : ""; + const long = typeof o.long === "string" ? o.long : ""; + return short || long ? { short, long } : null; +}; + +export const formatErrorMessage = (e: unknown): string => { + const envelope = toWailsEnvelope(e); + + // Prefer the structured { short, long } the daemon classifier produced. + const classified = toClassifiedError(envelope?.cause) ?? toClassifiedError(envelope); + if (classified) { + const { short, long } = classified; + if (short && long && long !== short) return `${short} Details: ${long}`; + if (short) return short; + if (long) return long; + } + + // Unclassified (a service returned the raw daemon error) + const message = envelope?.message; + if (typeof message === "string" && message) return message; + if (e instanceof Error) return e.message; + return String(e); +}; + +export type ErrorDialogOptions = { + Title: string; + Message: string; +}; + +export function errorDialog(options: ErrorDialogOptions): Promise { + return WindowManager.OpenError(options.Title, options.Message); +} diff --git a/client/ui/frontend/src/lib/formatters.ts b/client/ui/frontend/src/lib/formatters.ts new file mode 100644 index 000000000..231967a38 --- /dev/null +++ b/client/ui/frontend/src/lib/formatters.ts @@ -0,0 +1,44 @@ +export const formatBytes = (bytes: number, decimals: number = 2): string => { + if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; + + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k))); + + return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i]; +}; + +export const latencyColor = (ms: number): string => { + if (ms <= 0) return "text-nb-gray-400"; + if (ms < 100) return "text-green-400"; + return "text-yellow-400"; +}; + +export const formatRelative = (unixSeconds: number, nowMs: number = Date.now()): string | null => { + if (!Number.isFinite(unixSeconds) || unixSeconds <= 0) return null; + const diff = Math.max(0, Math.floor(nowMs / 1000 - unixSeconds)); + if (diff < 60) return `${diff}s ago`; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; +}; + +// Base domain is operator-configurable, so cut at the first dot rather than match a known suffix. +export const shortenDns = (fqdn: string | undefined | null): string => { + if (!fqdn) return ""; + const dot = fqdn.indexOf("."); + return dot === -1 ? fqdn : fqdn.slice(0, dot); +}; + +// Countdown clock: mm:ss, widening to hh:mm:ss / dd:hh:mm:ss as the duration grows. +export const formatRemaining = (seconds: number): string => { + const s = Math.max(0, Math.trunc(seconds)); + const days = Math.floor(s / 86400); + const hours = Math.floor((s % 86400) / 3600); + const minutes = Math.floor((s % 3600) / 60); + const secs = s % 60; + const pad = (n: number) => String(n).padStart(2, "0"); + if (days > 0) return `${pad(days)}:${pad(hours)}:${pad(minutes)}:${pad(secs)}`; + if (hours > 0) return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`; + return `${pad(minutes)}:${pad(secs)}`; +}; diff --git a/client/ui/frontend/src/lib/i18n.ts b/client/ui/frontend/src/lib/i18n.ts new file mode 100644 index 000000000..dc7fc7902 --- /dev/null +++ b/client/ui/frontend/src/lib/i18n.ts @@ -0,0 +1,103 @@ +import i18next from "i18next"; +import { initReactI18next } from "react-i18next"; +import { Events } from "@wailsio/runtime"; + +import { Preferences, I18n } from "@bindings/services"; +import { type LanguageCode } from "@bindings/i18n/models.js"; + +// Relative path on purpose — alias globs (`@/…`) silently match nothing in some Vite dev setups. +type BundleEntry = { message: string; description?: string }; +const bundleModules = import.meta.glob>( + "../../../i18n/locales/*/common.json", + { eager: true, import: "default" }, +); + +const resources: Record }> = {}; +for (const path in bundleModules) { + const match = /locales\/([^/]+)\/common\.json$/.exec(path); + if (match) { + const entries = bundleModules[path]; + const messages: Record = {}; + for (const key in entries) { + messages[key] = entries[key].message; + } + resources[match[1]] = { common: messages }; + } +} + +function detectBrowserLanguage(available: string[]): string | null { + const tags = [navigator.language, ...(navigator.languages ?? [])].filter( + (tag): tag is string => typeof tag === "string" && tag.length > 0, + ); + const byLower = new Map(available.map((code) => [code.toLowerCase(), code])); + for (const tag of tags) { + const lower = tag.toLowerCase(); + const exact = byLower.get(lower); + if (exact) return exact; + const base = byLower.get(lower.split("-")[0]); + if (base) return base; + } + return null; +} + +// An empty persisted language code is the Go-side signal for first run. +export async function initI18n(): Promise { + const available = Object.keys(resources); + let language = "en"; + let firstRun = false; + try { + const prefs = await Preferences.Get(); + if (prefs?.language) { + language = prefs.language; + } else { + firstRun = true; + language = detectBrowserLanguage(available) ?? "en"; + } + } catch (e) { + console.warn("read preferences for language failed, defaulting to en", e); + } + + if (firstRun) { + Preferences.SetLanguage(language as LanguageCode).catch((err: unknown) => + console.warn("persist detected language failed", err), + ); + } + + await i18next.use(initReactI18next).init({ + lng: language, + fallbackLng: "en", + defaultNS: "common", + ns: ["common"], + resources, + interpolation: { + prefix: "{", + suffix: "}", + escapeValue: false, + }, + returnNull: false, + }); + + syncDocumentLang(); + i18next.on("languageChanged", syncDocumentLang); + + Events.On("netbird:preferences:changed", (e) => { + const next = e.data?.language; + if (next && next !== i18next.language) { + i18next.changeLanguage(next).catch((err: unknown) => { + console.error("changeLanguage failed", err); + }); + } + }); +} + +function syncDocumentLang() { + if (typeof document !== "undefined") { + document.documentElement.lang = i18next.language; + } +} + +export async function loadLanguages() { + return I18n.Languages(); +} + +export default i18next; diff --git a/client/ui/frontend/src/lib/logs.ts b/client/ui/frontend/src/lib/logs.ts new file mode 100644 index 000000000..499023fa3 --- /dev/null +++ b/client/ui/frontend/src/lib/logs.ts @@ -0,0 +1,139 @@ +import { UILog } from "@bindings/services"; + +type Level = "trace" | "debug" | "info" | "warn" | "error"; + +const METHOD_LEVELS: Record = { + trace: "trace", + debug: "debug", + log: "info", + info: "info", + warn: "warn", + error: "error", +}; + +const IGNORED_SOURCES = new Set(["welcome.ts"]); + +const RATE_LIMIT = 50; +const RATE_WINDOW_MS = 1000; + +let installed = false; +let inForward = false; +let windowStart = 0; +let windowCount = 0; + +function describeCause(rawCause: unknown): string { + if (rawCause instanceof Error) return `${rawCause.name}: ${rawCause.message}`; + if (typeof rawCause === "object" && rawCause !== null) { + try { + return JSON.stringify(rawCause); + } catch { + // Circular ref — fall through to a tag instead of "[object Object]". + return `<${rawCause.constructor?.name ?? "object"}>`; + } + } + return String(rawCause); +} + +function formatCause(rawCause: unknown): string { + if (rawCause === undefined) return ""; + return `\ncaused by ${describeCause(rawCause)}`; +} + +// WebKit (macOS WKWebView) omits the "Name: message" header from Error.stack, +// so a bare stack hides the real cause. Prepend name+message, then the stack. +function formatError(e: Error): string { + const head = `${e.name}: ${e.message}`; + const cause = formatCause((e as { cause?: unknown }).cause); + if (!e.stack) return `${head}${cause}`; + if (e.stack.startsWith(head)) return `${head}${cause}`; + return `${head}${cause}\n${e.stack}`; +} + +function format(args: unknown[]): string { + return args + .map((a) => { + if (typeof a === "string") return a; + if (a instanceof Error) return formatError(a); + try { + return JSON.stringify(a); + } catch { + return String(a); + } + }) + .join(" "); +} + +function parseStackLine(line: string): string { + // Find the file:line:col tail at the end of the path. + const colonCol = line.lastIndexOf(":"); + if (colonCol <= 0) return ""; + const colonLine = line.lastIndexOf(":", colonCol - 1); + if (colonLine <= 0) return ""; + const col = line.slice(colonCol + 1); + const lineNo = line.slice(colonLine + 1, colonCol); + if (!/^\d+$/.test(col) || !/^\d+$/.test(lineNo)) return ""; + const before = line.slice(0, colonLine); + const sep = Math.max( + before.lastIndexOf("/"), + before.lastIndexOf("\\"), + before.lastIndexOf("("), + before.lastIndexOf(" "), + ); + const file = before.slice(sep + 1); + if (!file.includes(".")) return ""; + return `${file}:${lineNo}`; +} + +function callerSource(): string { + const stack = new Error().stack; + if (!stack) return ""; + for (const line of stack.split("\n").slice(1)) { + if (line.includes("/logs.ts")) continue; + const parsed = parseStackLine(line); + if (parsed) return parsed; + } + return ""; +} + +function forward(level: Level, args: unknown[]) { + if (inForward) return; + inForward = true; + try { + const now = Date.now(); + if (now - windowStart >= RATE_WINDOW_MS) { + windowStart = now; + windowCount = 0; + } + if (++windowCount > RATE_LIMIT) return; + + const source = callerSource(); + if (IGNORED_SOURCES.has(source.split(":")[0])) return; + // Don't touch console here — it would recurse back into forward(). + UILog.Log(level, source, format(args)).catch(() => {}); + } catch { + // Swallow — log forwarding must never throw back into the caller. + } finally { + inForward = false; + } +} + +export function initLogForwarding() { + if (installed) return; + installed = true; + + const c = console as unknown as Record void>; + for (const [method, level] of Object.entries(METHOD_LEVELS)) { + const original = c[method]?.bind(console); + c[method] = (...args: unknown[]) => { + original?.(...args); + forward(level, args); + }; + } + + globalThis.addEventListener("error", (e) => { + forward("error", [`uncaught error: ${e.message}`, e.error ?? ""]); + }); + globalThis.addEventListener("unhandledrejection", (e) => { + forward("error", ["unhandled promise rejection:", e.reason]); + }); +} diff --git a/client/ui/frontend/src/lib/platform.ts b/client/ui/frontend/src/lib/platform.ts new file mode 100644 index 000000000..c256dde23 --- /dev/null +++ b/client/ui/frontend/src/lib/platform.ts @@ -0,0 +1,39 @@ +import { System } from "@wailsio/runtime"; + +export type Platform = { + isWindows: boolean; + isMacOS: boolean; +}; + +let cached: Platform | null = null; + +export async function initPlatform(): Promise { + if (cached) return; + + const syncIsMac = System.IsMac(); + const syncIsWindows = System.IsWindows(); + + let env: Awaited> | null = null; + try { + env = await System.Environment(); + } catch (e) { + console.error("[platform] System.Environment() threw:", e); + } + + const os = (env?.OS ?? "").toLowerCase(); + cached = { + isWindows: os ? os === "windows" : syncIsWindows, + isMacOS: os ? os === "darwin" : syncIsMac, + }; +} + +function get(): Platform { + if (!cached) { + throw new Error("platform: initPlatform() must complete before sync getters are used"); + } + return cached; +} + +export const isWindows = (): boolean => get().isWindows; +export const isMacOS = (): boolean => get().isMacOS; +export const isLinux = (): boolean => !get().isWindows && !get().isMacOS; diff --git a/client/ui/frontend/src/lib/sorting.ts b/client/ui/frontend/src/lib/sorting.ts new file mode 100644 index 000000000..70622f705 --- /dev/null +++ b/client/ui/frontend/src/lib/sorting.ts @@ -0,0 +1,27 @@ +// Stable, order-preserving reconciliation for lists that re-fetch from the daemon +// on every status push (peers, networks, profiles). Re-sorting on each refresh would +// make rows jump around under the user, so instead: +// - items already on screen keep their existing order (from `prev`), +// - items that vanished are dropped, +// - newly-arrived items are sorted among themselves (`compareFresh`) and appended. +// Net effect: the only visible movement is new rows landing at the bottom. +// +// Must stay pure and idempotent: callers write the returned `order` into a ref +// during render (useMemo), so a rerun must reproduce the first pass — never +// branch on run count or read external mutable state. +export function reconcileOrder( + prev: string[], + items: T[], + keyOf: (item: T) => string, + compareFresh: (a: T, b: T) => number, +): { order: string[]; items: T[] } { + const byKey = new Map(items.map((i) => [keyOf(i), i])); + const kept = prev.filter((k) => byKey.has(k)); + const known = new Set(kept); + const fresh = items + .filter((i) => !known.has(keyOf(i))) + .sort(compareFresh) + .map(keyOf); + const order = [...kept, ...fresh]; + return { order, items: order.map((k) => byKey.get(k)!) }; +} diff --git a/client/ui/frontend/src/lib/welcome.ts b/client/ui/frontend/src/lib/welcome.ts new file mode 100644 index 000000000..d9df3fc8d --- /dev/null +++ b/client/ui/frontend/src/lib/welcome.ts @@ -0,0 +1,25 @@ +const ART = ` + _ __ __ ____ _ __ ______ __ __ __ + / | / /__ / /_/ __ )(_)________/ / / ____/___ ___ / /_ / / / / + / |/ / _ \\/ __/ __ / / ___/ __ / / / __/ __ \`__ \\/ __ \\/ /_/ / + / /| / __/ /_/ /_/ / / / / /_/ / / /_/ / / / / / / /_/ / __ / +/_/ |_/\\___/\\__/_____/_/_/ \\__,_/ \\____/_/ /_/ /_/_.___/_/ /_/ +`; + +export function welcome() { + const message = `%c${ART}%c +NetBird — The Only Secure Access Platform You'll Ever Need. + +WEBSITE: https://netbird.io/ +WE'RE HIRING: https://netbird.io/careers +OPEN SOURCE: https://github.com/netbirdio/netbird +`; + + // Intentional NetBird ASCII banner in the devtools console. + // eslint-disable-next-line no-console + console.log( + message, + "color: #f68330; font-family: monospace; font-weight: normal; line-height: 1;", + "color: #f5f5f5; font-family: monospace; font-weight: normal; line-height: 1.4;", + ); +} diff --git a/client/ui/frontend/src/modules/auto-update/UpdateBadge.tsx b/client/ui/frontend/src/modules/auto-update/UpdateBadge.tsx new file mode 100644 index 000000000..3345e0a9f --- /dev/null +++ b/client/ui/frontend/src/modules/auto-update/UpdateBadge.tsx @@ -0,0 +1,27 @@ +import { forwardRef, type HTMLAttributes } from "react"; +import { ArrowUpCircleIcon } from "lucide-react"; +import { cn } from "@/lib/cn"; + +type Props = HTMLAttributes & { + size?: number; +}; + +export const UpdateBadge = forwardRef(function UpdateBadge( + { size = 15, className, ...rest }, + ref, +) { + return ( +
+ + +
+ ); +}); diff --git a/client/ui/frontend/src/modules/auto-update/UpdateInProgressDialog.tsx b/client/ui/frontend/src/modules/auto-update/UpdateInProgressDialog.tsx new file mode 100644 index 000000000..1519613e0 --- /dev/null +++ b/client/ui/frontend/src/modules/auto-update/UpdateInProgressDialog.tsx @@ -0,0 +1,187 @@ +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSearchParams } from "react-router-dom"; +import { Loader2, XCircle } from "lucide-react"; +import { Update as UpdateSvc, WindowManager } from "@bindings/services"; +import { Button } from "@/components/buttons/Button"; +import { ConfirmDialog } from "@/components/dialog/ConfirmDialog"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import { SquareIcon } from "@/components/SquareIcon"; +import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow"; + +const TIMEOUT_MS = 15 * 60 * 1000; +const POLL_INTERVAL_MS = 2000; +// Sustained gRPC failure during install is taken as success (installer restarts the daemon mid-flight). +const DAEMON_DOWN_GRACE_MS = 5000; +const WINDOW_WIDTH = 360; + +type Phase = + | { kind: "running" } + | { kind: "timeout" } + | { kind: "canceled" } + | { kind: "failed"; message: string }; + +export default function UpdateInProgressDialog() { + const { t } = useTranslation(); + const [params] = useSearchParams(); + const version = params.get("version") ?? ""; + const [phase, setPhase] = useState({ kind: "running" }); + const phaseRef = useRef(phase); + phaseRef.current = phase; + const contentRef = useAutoSizeWindow(WINDOW_WIDTH); + + useEffect(() => { + let cancelled = false; + let done = false; + let timer: ReturnType | null = null; + const start = Date.now(); + let firstUnreachableAt: number | null = null; + + const poll = async () => { + if (cancelled || done) return; + if (phaseRef.current.kind !== "running") return; + + if (Date.now() - start > TIMEOUT_MS) { + done = true; + setPhase({ kind: "timeout" }); + return; + } + + try { + const r = await UpdateSvc.GetInstallerResult(); + if (cancelled || done || phaseRef.current.kind !== "running") return; + firstUnreachableAt = null; + if (r.success) { + done = true; + UpdateSvc.Quit().catch(console.error); + return; + } + if (r.errorMsg) { + done = true; + setPhase(mapInstallError(r.errorMsg)); + return; + } + } catch { + if (cancelled || done || phaseRef.current.kind !== "running") return; + const now = Date.now(); + if (firstUnreachableAt === null) { + firstUnreachableAt = now; + } else if (now - firstUnreachableAt >= DAEMON_DOWN_GRACE_MS) { + done = true; + UpdateSvc.Quit().catch(console.error); + return; + } + } + + if (!cancelled && !done) { + timer = setTimeout(poll, POLL_INTERVAL_MS); + } + }; + + timer = setTimeout(poll, POLL_INTERVAL_MS); + + return () => { + cancelled = true; + if (timer) clearTimeout(timer); + }; + }, []); + + const isError = phase.kind !== "running"; + const errorInfo = isError ? classifyPhase(phase, version, t) : null; + const updatingHeading = version + ? t("update.overlay.updatingVersion", { version }) + : t("update.overlay.updating"); + + return ( + + {isError ? ( + + ) : ( + + )} + +
+ + {errorInfo ? errorInfo.title : updatingHeading} + + + {errorInfo ? ( + <> + {errorInfo.description} + {errorInfo.message && ( + <> +
+ + {errorInfo.message} + + + )} + + ) : ( + t("update.overlay.description") + )} +
+
+ + {isError && ( + + + + )} +
+ ); +} + +function mapInstallError(msg: string): Phase { + const m = msg.trim().toLowerCase(); + if (m === "") return { kind: "failed", message: "" }; + if (m.includes("deadline exceeded") || m.includes("timeout") || m.includes("timed out")) { + return { kind: "timeout" }; + } + if (m.includes("canceled") || m.includes("cancelled") || m.includes("cancel")) { + return { kind: "canceled" }; + } + return { kind: "failed", message: msg }; +} + +type Variant = { title: string; description: string; message?: string }; + +function classifyPhase( + phase: Phase, + version: string, + t: (key: string, options?: Record) => string, +): Variant { + const target = version + ? t("update.overlay.error.targetVersion", { version }) + : t("update.overlay.error.targetFallback"); + switch (phase.kind) { + case "timeout": + return { + title: t("update.overlay.error.timeoutTitle"), + description: t("update.overlay.error.timeoutDescription", { target }), + }; + case "canceled": + return { + title: t("update.overlay.error.canceledTitle"), + description: t("update.overlay.error.canceledDescription", { target }), + }; + case "failed": + return { + title: t("update.overlay.error.failTitle"), + description: t("update.overlay.error.failDescription", { target }), + message: phase.message || t("update.overlay.error.unknownMessage"), + }; + default: + return { title: "", description: "" }; + } +} diff --git a/client/ui/frontend/src/modules/auto-update/UpdateVersionCard.tsx b/client/ui/frontend/src/modules/auto-update/UpdateVersionCard.tsx new file mode 100644 index 000000000..4fb5e2586 --- /dev/null +++ b/client/ui/frontend/src/modules/auto-update/UpdateVersionCard.tsx @@ -0,0 +1,96 @@ +import { type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { Browser } from "@wailsio/runtime"; +import { DownloadIcon, NotepadText } from "lucide-react"; +import { Button } from "@/components/buttons/Button"; +import { useClientVersion } from "@/contexts/ClientVersionContext"; +import { cn } from "@/lib/cn"; + +const GITHUB_RELEASES = "https://github.com/netbirdio/netbird/releases/latest"; + +function openUrl(url: string) { + Browser.OpenURL(url).catch(() => { + window.open(url, "_blank"); + }); +} + +export function UpdateVersionCard() { + const { t } = useTranslation(); + const { updateVersion, enforced, triggerUpdate } = useClientVersion(); + + if (updateVersion) { + const titleKey = enforced + ? "update.card.versionAvailableInstall" + : "update.card.versionAvailableDownload"; + return ( + +
+ {t(titleKey, { version: updateVersion })} + + {t("update.card.whatsNew")} + +
+ {enforced ? ( + + ) : ( + + )} +
+ ); + } + + return ( + +
+ {t("update.card.onLatestVersion")} +

{t("update.card.autoCheckInterval")}

+
+ +
+ ); +} + +function Card({ children, className }: Readonly<{ children: ReactNode; className?: string }>) { + return ( +
+ {children} +
+ ); +} + +function Title({ children }: Readonly<{ children: ReactNode }>) { + return

{children}

; +} + +function Link({ url, children }: Readonly<{ url: string; children: ReactNode }>) { + return ( + + ); +} diff --git a/client/ui/frontend/src/modules/error/ErrorDialog.tsx b/client/ui/frontend/src/modules/error/ErrorDialog.tsx new file mode 100644 index 000000000..f7929fad2 --- /dev/null +++ b/client/ui/frontend/src/modules/error/ErrorDialog.tsx @@ -0,0 +1,64 @@ +import { useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useSearchParams } from "react-router-dom"; +import { AlertCircleIcon } from "lucide-react"; +import { Button } from "@/components/buttons/Button"; +import { ConfirmDialog } from "@/components/dialog/ConfirmDialog"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import { SquareIcon } from "@/components/SquareIcon"; +import { WindowManager } from "@bindings/services"; +import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow"; + +const WINDOW_WIDTH = 380; + +export default function ErrorDialog() { + const { t } = useTranslation(); + const contentRef = useAutoSizeWindow(WINDOW_WIDTH); + const [params] = useSearchParams(); + + const title = params.get("title") || t("window.title.error"); + const message = params.get("message") || ""; + + const close = useCallback(() => { + WindowManager.CloseError().catch(console.error); + }, []); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") close(); + }; + globalThis.addEventListener("keydown", onKey); + return () => globalThis.removeEventListener("keydown", onKey); + }, [close]); + + return ( + + + +
+ + {title} + + {message && ( + + {message} + + )} +
+ + + + +
+ ); +} diff --git a/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx b/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx new file mode 100644 index 000000000..efbd1ee84 --- /dev/null +++ b/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx @@ -0,0 +1,90 @@ +import { useCallback, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { useSearchParams } from "react-router-dom"; +import { Events } from "@wailsio/runtime"; +import { Loader2 } from "lucide-react"; +import { Connection } from "@bindings/services"; +import { Button } from "@/components/buttons/Button"; +import { ConfirmDialog } from "@/components/dialog/ConfirmDialog"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import { SquareIcon } from "@/components/SquareIcon"; +import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow"; +import { errorDialog, formatErrorMessage } from "@/lib/errors"; + +const EVENT_CANCEL = "browser-login:cancel"; +const WINDOW_WIDTH = 360; + +export default function LoginWaitingForBrowserDialog() { + const { t } = useTranslation(); + const [params] = useSearchParams(); + const uri = params.get("uri") ?? ""; + const contentRef = useAutoSizeWindow(WINDOW_WIDTH); + const openedRef = useRef(false); + + const reportOpenFailure = useCallback( + (e: unknown) => { + void errorDialog({ + Title: t("browserLogin.openFailedTitle"), + Message: formatErrorMessage(e), + }); + }, + [t], + ); + + // Open the browser only after mount, or it lands on top of the still-hidden popup. + useEffect(() => { + if (!uri || openedRef.current) return; + openedRef.current = true; + Connection.OpenURL(uri).catch(reportOpenFailure); + }, [uri, reportOpenFailure]); + + const tryAgain = useCallback(() => { + if (!uri) return; + Connection.OpenURL(uri).catch(reportOpenFailure); + }, [uri, reportOpenFailure]); + + const cancel = useCallback(() => { + Events.Emit(EVENT_CANCEL).catch((err: unknown) => + console.error("emit browser-login cancel", err), + ); + }, []); + + return ( + + + +
+ + {t("browserLogin.title")} + + + {t("browserLogin.notSeeing")}{" "} + + +
+ + + + +
+ ); +} diff --git a/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx b/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx new file mode 100644 index 000000000..c2a648278 --- /dev/null +++ b/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx @@ -0,0 +1,410 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Events } from "@wailsio/runtime"; +import { Connection, WindowManager } from "@bindings/services"; +import { ToggleSwitch } from "@/components/switches/ToggleSwitch.tsx"; +import { useStatus } from "@/contexts/StatusContext.tsx"; +import { useProfile } from "@/contexts/ProfileContext.tsx"; +import { cn } from "@/lib/cn.ts"; +import { errorDialog, formatErrorMessage } from "@/lib/errors.ts"; +import { + startConnection, + EVENT_BROWSER_LOGIN_CANCEL, + EVENT_TRIGGER_LOGIN, +} from "@/lib/connection.ts"; +import { CopyToClipboard } from "@/components/CopyToClipboard"; +import { TruncatedText } from "@/components/TruncatedText"; +import { shortenDns } from "@/lib/formatters"; +import { contentTop } from "@/components/empty-state/EmptyState"; +import { useFocusVisible } from "@/hooks/useFocusVisible"; +import { Check as CheckIcon, ChevronDownIcon, Copy as CopyIcon } from "lucide-react"; +import * as Popover from "@radix-ui/react-popover"; +import netbirdFullLogo from "@/assets/logos/netbird-full.svg"; + +enum ConnectionState { + Disconnected = "disconnected", + Connecting = "connecting", + Connected = "connected", + Disconnecting = "disconnecting", +} + +const STATUS_KEY: Record = { + [ConnectionState.Disconnected]: "connect.status.disconnected", + [ConnectionState.Connecting]: "connect.status.connecting", + [ConnectionState.Connected]: "connect.status.connected", + [ConnectionState.Disconnecting]: "connect.status.disconnecting", +}; + +const NEEDS_LOGIN_STATES = new Set(["NeedsLogin", "SessionExpired", "LoginFailed"]); + +const FORCE_TOGGLE_DELAY_MS = 7000; + +const errorMessage = formatErrorMessage; + +export const MainConnectionStatusSwitch = () => { + const { t } = useTranslation(); + const { status, refresh } = useStatus(); + const { activeProfileId, username } = useProfile(); + + const daemonState = status?.status ?? "Idle"; + const needsLogin = NEEDS_LOGIN_STATES.has(daemonState); + const unreachable = daemonState === "DaemonUnavailable"; + + type Action = "connect" | "logging-in" | "disconnect" | null; + const [action, setAction] = useState(null); + + const loginGuard = useRef(false); + const driveLogin = useCallback(() => { + if (loginGuard.current) return; + loginGuard.current = true; + setAction("logging-in"); + void startConnection(() => { + loginGuard.current = false; + setAction(null); + refresh().catch((err: unknown) => console.error("refresh after login failed", err)); + }); + }, [refresh]); + + const connState: ConnectionState = useMemo(() => { + if (action === "disconnect" && daemonState === "Connected") { + return ConnectionState.Disconnecting; + } + if ((action === "connect" || action === "logging-in") && daemonState !== "Connected") { + return ConnectionState.Connecting; + } + switch (daemonState) { + case "Connected": + return ConnectionState.Connected; + case "Connecting": + return ConnectionState.Connecting; + case "Idle": + case "NeedsLogin": + case "LoginFailed": + case "SessionExpired": + case "DaemonUnavailable": + return ConnectionState.Disconnected; + default: + return ConnectionState.Disconnected; + } + }, [daemonState, action]); + + const connect = async () => { + setAction("connect"); + try { + await Connection.Up({ + profileName: activeProfileId, + username, + }); + await refresh(); + } catch (e) { + setAction(null); + await refresh(); + await errorDialog({ + Title: t("connect.error.connectTitle"), + Message: errorMessage(e), + }); + } + }; + + const disconnect = async () => { + setAction("disconnect"); + try { + await Connection.Down(); + await refresh(); + } catch (e) { + setAction(null); + await refresh(); + await errorDialog({ + Title: t("connect.error.disconnectTitle"), + Message: errorMessage(e), + }); + } + }; + + const sawConnectingRef = useRef(false); + + useEffect(() => { + if (action === null) { + sawConnectingRef.current = false; + return; + } + if (daemonState === "Connecting") { + sawConnectingRef.current = true; + } + if (action === "connect") { + if (needsLogin) { + driveLogin(); + return; + } + if (daemonState === "Connected" || unreachable) { + setAction(null); + return; + } + if (sawConnectingRef.current && daemonState === "Idle") { + setAction(null); + } + return; + } + if (action === "disconnect") { + if (daemonState === "Idle" || daemonState === "Disconnected" || unreachable) { + setAction(null); + } + } + }, [action, daemonState, needsLogin, unreachable, driveLogin]); + + useEffect(() => { + const off = Events.On(EVENT_TRIGGER_LOGIN, () => { + driveLogin(); + }); + return () => off(); + }, [driveLogin]); + + const handleSwitch = (next: boolean) => { + if (unreachable) return; + if (isTransitioning) { + if (canForceCancel) void forceCancel(); + return; + } + if (action !== null) return; + if (needsLogin) { + driveLogin(); + return; + } + if (next && connState === ConnectionState.Disconnected) { + void connect(); + } else if (!next && connState === ConnectionState.Connected) { + void disconnect(); + } + }; + + const isTransitioning = + connState === ConnectionState.Connecting || connState === ConnectionState.Disconnecting; + const isOn = + connState === ConnectionState.Connected || connState === ConnectionState.Connecting; + + const [canForceCancel, setCanForceCancel] = useState(false); + useEffect(() => { + if (!isTransitioning) { + setCanForceCancel(false); + return; + } + const id = setTimeout(() => setCanForceCancel(true), FORCE_TOGGLE_DELAY_MS); + return () => clearTimeout(id); + }, [isTransitioning]); + + const forceCancel = async () => { + if (action === "logging-in") { + Events.Emit(EVENT_BROWSER_LOGIN_CANCEL).catch((err: unknown) => + console.error("emit browser-login cancel failed", err), + ); + } + WindowManager.CloseBrowserLogin().catch((err: unknown) => + console.warn("close browser-login window failed", err), + ); + setAction("disconnect"); + try { + await Connection.Down(); + await refresh(); + } catch (e) { + setAction(null); + await refresh(); + await errorDialog({ + Title: t("connect.error.disconnectTitle"), + Message: errorMessage(e), + }); + } + }; + const show = connState === ConnectionState.Connected; + const fqdn = status?.local.fqdn || ""; + const ip = status?.local.ip || ""; + const ipv6 = status?.local.ipv6 || ""; + + return ( +
+ {"NetBird"} + + + +
+

+ {t(STATUS_KEY[connState])} +

+ + + + +
+
+ ); +}; + +const LocalIpLine = ({ ip, ipv6, show }: { ip: string; ipv6: string; show: boolean }) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const isFocusVisible = useFocusVisible(); + const hasV6 = !!ipv6; + + if (!hasV6) { + return ( + + + {ip || " "} + + + ); + } + + return ( +
+ + + + + + e.preventDefault()} + className={cn( + "z-50 min-w-64 max-w-[280px] overflow-hidden", + "rounded-lg border border-nb-gray-900 bg-nb-gray-935", + "p-1 text-nb-gray-200 shadow-lg outline-none", + "flex flex-col", + )} + > + +
+ + + + +
+ ); +}; + +const IpRow = ({ value }: { value: string }) => { + const { t } = useTranslation(); + const [copied, setCopied] = useState(false); + const isFocusVisible = useFocusVisible(); + const handleClick = async () => { + if (!value) return; + try { + await navigator.clipboard.writeText(value); + setCopied(true); + setTimeout(() => setCopied(false), 500); + } catch (e) { + console.warn("copy IP to clipboard failed", e); + } + }; + return ( + + ); +}; diff --git a/client/ui/frontend/src/modules/main/MainExitNodeSwitcher.tsx b/client/ui/frontend/src/modules/main/MainExitNodeSwitcher.tsx new file mode 100644 index 000000000..fc7ce37b2 --- /dev/null +++ b/client/ui/frontend/src/modules/main/MainExitNodeSwitcher.tsx @@ -0,0 +1,256 @@ +import { forwardRef, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import * as Popover from "@radix-ui/react-popover"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { Command } from "cmdk"; +import { Check, ChevronsUpDown, type LucideProps, SquareArrowUpRight } from "lucide-react"; +import { cn } from "@/lib/cn"; +import { TruncatedText } from "@/components/TruncatedText"; +import { useNetworks } from "@/contexts/NetworksContext"; +import { useStatus } from "@/contexts/StatusContext"; +import { useFocusVisible } from "@/hooks/useFocusVisible"; + +const NONE_VALUE = "__none__"; + +export const MainExitNodeSwitcher = () => { + const { t } = useTranslation(); + const { status } = useStatus(); + const { exitNodes, toggleExitNode } = useNetworks(); + const active = exitNodes.find((n) => n.selected) ?? null; + const isConnected = status?.status === "Connected"; + const hasAny = exitNodes.length > 0; + const disabled = !isConnected || !hasAny; + + const [open, setOpen] = useState(false); + const listRef = useRef(null); + + const handleTriggerKeyDown = (e: React.KeyboardEvent) => { + if (open || disabled) return; + if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + setOpen(true); + } + }; + + const handleSelect = (next: string) => { + setOpen(false); + if (next === NONE_VALUE) { + if (active) + toggleExitNode(active.id, true).catch((err: unknown) => + console.error("toggle exit node failed", err), + ); + return; + } + if (active?.id === next) return; + toggleExitNode(next, false).catch((err: unknown) => + console.error("toggle exit node failed", err), + ); + }; + + const title = active ? active.id : t("exitNodes.card.title"); + const activeDescription = active + ? t("exitNodes.card.statusActive") + : t("exitNodes.card.statusInactive"); + const description = hasAny ? activeDescription : t("exitNodes.empty.title"); + + return ( + + + + + + { + e.preventDefault(); + listRef.current?.focus(); + }} + style={{ width: "var(--radix-popover-trigger-width)" }} + className={cn( + "wails-no-draggable z-50 select-none overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg", + "data-[state=open]:animate-in data-[state=closed]:animate-out", + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", + "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2", + "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + )} + > + e.stopPropagation()} + className={"outline-none focus:outline-none focus-visible:outline-none"} + > + + handleSelect(NONE_VALUE)} /> + {hasAny &&
} + {hasAny && ( + + + {exitNodes.map((n) => ( + handleSelect(n.id)} + /> + ))} + + + + + + )} + + + + + + ); +}; + +type TriggerProps = React.ButtonHTMLAttributes & { + title: string; + description: string; + active?: boolean; +}; + +const ExitNodeTriggerCard = forwardRef( + function ExitNodeTriggerCard( + { title, description, disabled, active = false, className, ...props }, + ref, + ) { + const isFocusVisible = useFocusVisible(); + return ( + + ); + }, +); + +type NoneRowProps = { + isActive: boolean; + onSelect: () => void; +}; + +const NoneRow = ({ isActive, onSelect }: NoneRowProps) => { + const { t } = useTranslation(); + return ( + + {t("exitNodes.dropdown.noneTitle")} + {isActive && ( + + )} + + ); +}; + +type ExitNodeRowProps = { + id: string; + label: string; + isActive: boolean; + onSelect: () => void; +}; + +const ExitNodeRow = ({ id, label, isActive, onSelect }: ExitNodeRowProps) => ( + + {label} + {isActive && } + +); + +const ExitNodeIcon = ({ size, ...props }: LucideProps) => ( + +); diff --git a/client/ui/frontend/src/modules/main/MainHeader.tsx b/client/ui/frontend/src/modules/main/MainHeader.tsx new file mode 100644 index 000000000..becfde47c --- /dev/null +++ b/client/ui/frontend/src/modules/main/MainHeader.tsx @@ -0,0 +1,192 @@ +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ArrowUpCircleIcon, + Check, + MoreVertical, + PanelsRightBottom, + RectangleVertical, + Settings, + type LucideIcon, +} from "lucide-react"; +import { WindowManager } from "@bindings/services"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/DropdownMenu"; +import { IconButton } from "@/components/buttons/IconButton"; +import { ProfileDropdown } from "@/modules/profiles/ProfileDropdown"; +import { useClientVersion } from "@/contexts/ClientVersionContext"; +import { cn } from "@/lib/cn"; +import { formatShortcut, useKeyboardShortcut } from "@/hooks/useKeyboardShortcut"; +import { useViewMode, type ViewMode } from "@/contexts/ViewModeContext"; +import { useRestrictions } from "@/contexts/RestrictionsContext"; +import { isWindows } from "@/lib/platform.ts"; + +const SETTINGS_SHORTCUT = { key: ",", cmd: true } as const; + +export const MainHeader = () => { + const { t } = useTranslation(); + const [menuOpen, setMenuOpen] = useState(false); + const { viewMode, setViewMode } = useViewMode(); + const { updateAvailable } = useClientVersion(); + const { mdm, features } = useRestrictions(); + + const openSettings = useCallback(() => { + setMenuOpen(false); + WindowManager.OpenSettings("").catch((err: unknown) => + console.error("open settings window failed", err), + ); + }, []); + + useKeyboardShortcut(SETTINGS_SHORTCUT, openSettings); + + const openAbout = () => { + setMenuOpen(false); + WindowManager.OpenSettings("about").catch((err: unknown) => + console.error("open settings (about) window failed", err), + ); + }; + + const openManageProfiles = () => { + WindowManager.OpenSettings("profiles").catch((err: unknown) => + console.error("open settings (profiles) window failed", err), + ); + }; + + const selectMode = (mode: ViewMode) => { + setMenuOpen(false); + setViewMode(mode); + }; + + const profileSlot = features.disableProfiles ? null : ( + + ); + + const settingsSlot = ( +
+ + + + + + {updateAvailable && ( + <> + +
+ + + {t("header.menu.updateAvailable")} + +
+
+ + + )} + +
+ + {t("header.menu.settings")} + + {formatShortcut(SETTINGS_SHORTCUT)} + +
+
+ {!mdm.disableAdvancedView && ( + <> + + selectMode("default")} + /> + selectMode("advanced")} + /> + + )} +
+
+ {updateAvailable && ( + + + + + )} +
+ ); + + return ( +
+ {/* Windows narrower width compensates for the OS frame Wails counts differently than macOS. + See https://github.com/wailsapp/wails/issues/3260 */} +
+
+
{profileSlot}
+
+
+
{settingsSlot}
+
+ ); +}; + +type ViewModeItemProps = { + icon: LucideIcon; + label: string; + selected: boolean; + onSelect: () => void; +}; + +const ViewModeItem = ({ icon: Icon, label, selected, onSelect }: ViewModeItemProps) => ( + +
+ + {label} + {selected && } +
+
+); diff --git a/client/ui/frontend/src/modules/main/MainPage.tsx b/client/ui/frontend/src/modules/main/MainPage.tsx new file mode 100644 index 000000000..c05b3a025 --- /dev/null +++ b/client/ui/frontend/src/modules/main/MainPage.tsx @@ -0,0 +1,114 @@ +import { MainConnectionStatusSwitch } from "@/modules/main/MainConnectionStatusSwitch.tsx"; +import { MainExitNodeSwitcher } from "@/modules/main/MainExitNodeSwitcher.tsx"; +import { MainHeader } from "@/modules/main/MainHeader.tsx"; +import { AppRightPanel } from "@/layouts/AppRightPanel.tsx"; +import { Navigation } from "@/modules/main/advanced/Navigation.tsx"; +import { cn } from "@/lib/cn"; +import { NavSectionProvider, useNavSection } from "@/contexts/NavSectionContext"; +import { ViewModeProvider, useViewMode } from "@/contexts/ViewModeContext"; +import { useEffect } from "react"; +import { NotConnectedState } from "@/components/empty-state/NotConnectedState"; +import { useStatus } from "@/contexts/StatusContext"; +import { Peers } from "@/modules/main/advanced/peers/Peers"; +import { Networks } from "@/modules/main/advanced/networks/Networks"; +import { NetworksProvider } from "@/contexts/NetworksContext"; +import { PeerDetailProvider, usePeerDetail } from "@/contexts/PeerDetailContext"; +import { useRestrictions } from "@/contexts/RestrictionsContext"; +import { PeerDetailPanel } from "@/modules/main/advanced/peers/PeerDetailPanel"; +import { isWindows } from "@/lib/platform.ts"; + +export const MainPage = () => { + return ( + + + + + + + + + ); +}; + +const MainBody = () => { + const { viewMode, setViewMode } = useViewMode(); + const { mdm, features } = useRestrictions(); + + // Force flip the view if MDM disabled advanced + useEffect(() => { + if (mdm.disableAdvancedView && viewMode === "advanced") { + setViewMode("default"); + } + }, [mdm.disableAdvancedView, viewMode, setViewMode]); + + const isAdvanced = viewMode === "advanced"; + + return ( +
+ {/* Windows narrower width compensates for the OS frame Wails counts differently than macOS. + See https://github.com/wailsapp/wails/issues/3260 */} +
+ + {!features.disableNetworks && ( +
+ +
+ )} +
+ {isAdvanced && ( + + + + )} +
+ ); +}; + +const AdvancedAppRightPanel = () => { + const { section } = useNavSection(); + const { selected } = usePeerDetail(); + const { status } = useStatus(); + const isConnected = status?.status === "Connected"; + + return ( + } + overlayOpen={selected !== null} + className={"m-5 ml-0"} + > +
{ + if (!el) return; + if (isConnected) el.removeAttribute("inert"); + else el.setAttribute("inert", ""); + }} + className={cn( + "flex min-h-0 min-w-0 flex-1 flex-col", + !isConnected && "pointer-events-none select-none", + )} + aria-hidden={!isConnected} + > + +
+ {section === "peers" && } + {section === "networks" && } +
+
+ {!isConnected && ( +
+ +
+ )} +
+ ); +}; diff --git a/client/ui/frontend/src/modules/main/advanced/Navigation.tsx b/client/ui/frontend/src/modules/main/advanced/Navigation.tsx new file mode 100644 index 000000000..8d69f3580 --- /dev/null +++ b/client/ui/frontend/src/modules/main/advanced/Navigation.tsx @@ -0,0 +1,134 @@ +import { type ComponentType, type KeyboardEvent, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { Layers3Icon, type LucideProps, MonitorSmartphoneIcon } from "lucide-react"; +import { cn } from "@/lib/cn"; +import { useNavSection, type NavSection } from "@/contexts/NavSectionContext"; +import { useStatus } from "@/contexts/StatusContext"; +import { useRestrictions } from "@/contexts/RestrictionsContext"; + +type TabEntry = { + value: NavSection; + label: string; + icon: ComponentType; +}; + +export const Navigation = () => { + const { t } = useTranslation(); + const { section, setSection } = useNavSection(); + const { status } = useStatus(); + const { features } = useRestrictions(); + const isConnected = status?.status === "Connected"; + + // Reset back to peers tab if mdm or feature flag flipped it + useEffect(() => { + if (features.disableNetworks && section === "networks") { + setSection("peers"); + } + }, [features.disableNetworks, section, setSection]); + + const tabs: TabEntry[] = [ + { + value: "peers", + label: t("nav.peers.title"), + icon: MonitorSmartphoneIcon, + }, + ]; + if (!features.disableNetworks) { + tabs.push({ + value: "networks", + label: t("nav.resources.title"), + icon: Layers3Icon, + }); + } + + const tabRefs = useRef>({}); + + const focusTab = (value: NavSection) => { + setSection(value); + requestAnimationFrame(() => tabRefs.current[value]?.focus()); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + const enabled = tabs.filter((t) => isConnected || t.value === section); + if (enabled.length < 2) return; + const currentIndex = enabled.findIndex((t) => t.value === section); + if (currentIndex === -1) return; + let nextIndex: number; + switch (e.key) { + case "ArrowRight": + nextIndex = (currentIndex + 1) % enabled.length; + break; + case "ArrowLeft": + nextIndex = (currentIndex - 1 + enabled.length) % enabled.length; + break; + case "Home": + nextIndex = 0; + break; + case "End": + nextIndex = enabled.length - 1; + break; + default: + return; + } + e.preventDefault(); + focusTab(enabled[nextIndex].value); + }; + + return ( +
+ {tabs.map((tab, index) => { + const isActive = tab.value === section; + const isDisabled = !isConnected && !isActive; + const isFirst = index === 0; + const isLast = index === tabs.length - 1; + const Icon = tab.icon; + return ( + + ); + })} +
+ ); +}; + +export type { NavSection } from "@/contexts/NavSectionContext"; diff --git a/client/ui/frontend/src/modules/main/advanced/networks/NetworkFilters.tsx b/client/ui/frontend/src/modules/main/advanced/networks/NetworkFilters.tsx new file mode 100644 index 000000000..ec1823f0b --- /dev/null +++ b/client/ui/frontend/src/modules/main/advanced/networks/NetworkFilters.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { CheckIcon, ChevronDown, ListFilter } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/cn"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/DropdownMenu"; + +export type NetworkFilter = "all" | "active" | "overlapping"; + +type Props = { + value: NetworkFilter; + onChange: (value: NetworkFilter) => void; + counts: Record; + disabled?: boolean; +}; + +export const NetworkFilters = ({ value, onChange, counts, disabled }: Props) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const filters: { value: NetworkFilter; label: string }[] = [ + { value: "all", label: t("networks.filter.all") }, + { value: "active", label: t("networks.filter.active") }, + { value: "overlapping", label: t("networks.filter.overlapping") }, + ]; + const active = filters.find((f) => f.value === value) ?? filters[0]; + + const handleSelect = (v: NetworkFilter) => { + onChange(v); + setOpen(false); + }; + + return ( + + + + + {active.label} ({counts[active.value]}) + + + + + {filters.map((f) => { + const checked = f.value === value; + return ( + handleSelect(f.value)} + role={"menuitemradio"} + aria-checked={checked} + className={"gap-2"} + > + + {f.label}{" "} + ({counts[f.value]}) + + + {checked && } + + + ); + })} + + + ); +}; diff --git a/client/ui/frontend/src/modules/main/advanced/networks/Networks.tsx b/client/ui/frontend/src/modules/main/advanced/networks/Networks.tsx new file mode 100644 index 000000000..f6fe23aaf --- /dev/null +++ b/client/ui/frontend/src/modules/main/advanced/networks/Networks.tsx @@ -0,0 +1,528 @@ +import { + type KeyboardEvent, + useEffect, + useMemo, + useRef, + useState, + type ComponentType, + type ReactNode, +} from "react"; +import { useTranslation } from "react-i18next"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; +import { GlobeIcon, Layers3Icon, type LucideProps, NetworkIcon, WorkflowIcon } from "lucide-react"; +import type { Network } from "@bindings/services/models.js"; +import { cn } from "@/lib/cn"; +import { reconcileOrder } from "@/lib/sorting"; +import { CopyToClipboard } from "@/components/CopyToClipboard"; +import { Tooltip } from "@/components/Tooltip"; +import { TruncatedText } from "@/components/TruncatedText"; +import { SearchInput } from "@/components/inputs/SearchInput"; +import { EmptyState } from "@/components/empty-state/EmptyState"; +import { NoResults } from "@/components/empty-state/NoResults"; +import { useStatus } from "@/contexts/StatusContext"; +import { useNetworks } from "@/contexts/NetworksContext"; +import { type NetworkFilter, NetworkFilters } from "./NetworkFilters"; + +// Daemon renders DNS-route prefixes (zero netip.Prefix) as "invalid Prefix". +const INVALID_PREFIX = "invalid Prefix"; + +const isDnsRoute = (n: Network): boolean => + n.domains.length > 0 && (!n.range || n.range === INVALID_PREFIX); + +type ResourceType = "host" | "subnet" | "domain"; + +const isHostCidr = (cidr: string): boolean => { + const [addr, bitsStr] = cidr.split("/"); + if (!addr || !bitsStr) return false; + const bits = Number(bitsStr); + const isV6 = addr.includes(":"); + return isV6 ? bits === 128 : bits === 32; +}; + +const resourceTypeOf = (n: Network): ResourceType => { + if (isDnsRoute(n)) return "domain"; + const primary = n.range.split(",")[0].trim(); + return isHostCidr(primary) ? "host" : "subnet"; +}; + +const resourceIconFor = (type: ResourceType): ComponentType => { + if (type === "host") return WorkflowIcon; + if (type === "domain") return GlobeIcon; + return NetworkIcon; +}; + +const buildOverlapMap = ( + routes: { id: string; range: string; domains: string[] }[], +): Map => { + const byRange = new Map(); + for (const r of routes) { + if (r.domains.length > 0) continue; + const arr = byRange.get(r.range) ?? []; + arr.push(r.id); + byRange.set(r.range, arr); + } + const out = new Map(); + for (const [range, ids] of byRange) { + if (ids.length > 1) out.set(range, ids); + } + return out; +}; + +export const Networks = () => { + const { t } = useTranslation(); + const { status } = useStatus(); + const isConnected = status?.status === "Connected"; + const { networkRoutes, toggleNetwork, setNetworksSelected } = useNetworks(); + const [search, setSearch] = useState(""); + const [filter, setFilter] = useState("all"); + const [scrollParent, setScrollParent] = useState(null); + const searchRef = useRef(null); + + useEffect(() => { + searchRef.current?.focus(); + }, []); + + const overlapGroups = useMemo(() => buildOverlapMap(networkRoutes), [networkRoutes]); + + const overlapById = useMemo(() => { + const map = new Map(); + for (const ids of overlapGroups.values()) { + for (const id of ids) map.set(id, ids); + } + return map; + }, [overlapGroups]); + + const counts = useMemo>( + () => ({ + all: networkRoutes.length, + active: networkRoutes.filter((r) => r.selected).length, + overlapping: overlapById.size, + }), + [networkRoutes, overlapById], + ); + + const orderRef = useRef([]); + const ordered = useMemo(() => { + const { order, items } = reconcileOrder( + orderRef.current, + networkRoutes, + (r) => r.id, + (a, b) => { + if (a.selected !== b.selected) return a.selected ? -1 : 1; + return a.id.localeCompare(b.id); + }, + ); + orderRef.current = order; + return items; + }, [networkRoutes]); + + const filtered = useMemo(() => { + const q = search.trim().toLowerCase(); + return ordered.filter((r) => { + if (filter === "active" && !r.selected) return false; + if (filter === "overlapping" && !overlapById.has(r.id)) return false; + if (q) { + const haystack = [r.id, r.range, ...r.domains].join(" ").toLowerCase(); + if (!haystack.includes(q)) return false; + } + return true; + }); + }, [ordered, search, filter, overlapById]); + + if (isConnected && networkRoutes.length === 0) { + return ( + + ); + } + + const selectedInView = filtered.filter((r) => r.selected).length; + const allSelected = filtered.length > 0 && selectedInView === filtered.length; + const bulkLabel = allSelected ? t("networks.bulk.disableAll") : t("networks.bulk.enableAll"); + + const onBulkClick = () => { + if (filtered.length === 0) return; + if (allSelected) { + setNetworksSelected( + filtered.map((r) => r.id), + false, + ).catch((err: unknown) => console.error("disable all networks failed", err)); + } else { + const ids = filtered.filter((r) => !r.selected).map((r) => r.id); + setNetworksSelected(ids, true).catch((err: unknown) => + console.error("enable all networks failed", err), + ); + } + }; + + return ( +
+
+
+ setSearch(e.target.value)} + /> +
+ +
+ {filtered.length === 0 ? ( + + ) : ( + + + {scrollParent && ( + + )} + + + + + + )} + {filtered.length > 0 && ( +
+ + {t("networks.bulk.selectionCount", { + selected: selectedInView, + total: filtered.length, + })} + + +
+ )} +
+ ); +}; + +type NetworksListProps = { + data: Network[]; + onToggle: (id: string, selected: boolean) => void; + scrollParent: HTMLElement; +}; + +const NetworksHeader = () =>
; + +const NetworksList = ({ data, onToggle, scrollParent }: NetworksListProps) => { + const virtuosoRef = useRef(null); + const rowRefs = useRef>(new Map()); + + const focusRow = (index: number) => { + if (index < 0 || index >= data.length) return; + const row = data[index]; + const tryFocus = () => { + const el = rowRefs.current.get(row.id); + if (el) { + el.focus(); + return true; + } + return false; + }; + if (!tryFocus()) { + virtuosoRef.current?.scrollToIndex({ index, behavior: "auto" }); + requestAnimationFrame(() => { + if (!tryFocus()) requestAnimationFrame(tryFocus); + }); + } + }; + + const handleRowKeyDown = (e: KeyboardEvent, index: number) => { + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + focusRow(Math.min(index + 1, data.length - 1)); + break; + case "ArrowUp": + e.preventDefault(); + focusRow(Math.max(index - 1, 0)); + break; + case "Home": + e.preventDefault(); + focusRow(0); + break; + case "End": + e.preventDefault(); + focusRow(data.length - 1); + break; + } + }; + + const setRowRef = (id: string, el: HTMLButtonElement | null) => { + if (el) rowRefs.current.set(id, el); + else rowRefs.current.delete(id); + }; + + const ctx = useMemo( + () => ({ onKeyDown: handleRowKeyDown, onToggle, setRowRef }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [data, onToggle], + ); + + return ( + + ref={virtuosoRef} + data={data} + customScrollParent={scrollParent} + increaseViewportBy={400} + computeItemKey={(_, n) => n.id} + components={{ Header: NetworksHeader }} + context={ctx} + itemContent={renderNetworkRow} + /> + ); +}; + +type NetworkRowContext = { + onKeyDown: (e: KeyboardEvent, index: number) => void; + onToggle: (id: string, selected: boolean) => void; + setRowRef: (id: string, el: HTMLButtonElement | null) => void; +}; + +const renderNetworkRow = (index: number, n: Network, ctx: NetworkRowContext): ReactNode => ( + +); + +type NetworkRowProps = { + network: Network; + index: number; + onKeyDown: (e: KeyboardEvent, index: number) => void; + onToggle: (id: string, selected: boolean) => void; + setRowRef: (id: string, el: HTMLButtonElement | null) => void; +}; + +const NetworkRow = ({ network: n, index, onKeyDown, onToggle, setRowRef }: NetworkRowProps) => { + const { t } = useTranslation(); + // Same handler is attached to the overlay button and to the network-id copy + // button so arrow nav works wherever focus sits inside the row. + const handleKey = (e: KeyboardEvent) => onKeyDown(e, index); + return ( +
+
+ ); +}; + +const ResourceIconBadge = ({ type }: { type: ResourceType }) => { + const Icon = resourceIconFor(type); + return ( +
+ +
+ ); +}; + +type SubtitleProps = { + network: Network; + onKeyDown: (e: KeyboardEvent) => void; +}; + +const Subtitle = ({ network, onKeyDown }: SubtitleProps) => { + if (isDnsRoute(network)) { + const domain = network.domains[0]; + const ips = network.resolvedIps[domain] ?? []; + return ; + } + + if (network.range && network.range !== INVALID_PREFIX) { + return ( +
+ + + +
+ ); + } + + return null; +}; + +type DomainSubtitleProps = { + domain: string; + ips: string[]; + onKeyDown: (e: KeyboardEvent) => void; +}; + +const DomainSubtitle = ({ domain, ips, onKeyDown }: DomainSubtitleProps) => { + const span = ( + + {domain} + + ); + return ( +
+ + {ips.length > 0 ? ( + } + delayDuration={300} + closeDelay={300} + side={"right"} + align={"start"} + alignOffset={-8} + interactive + keepOpenOnClick + contentClassName={cn( + "max-h-72 max-w-[18rem] overflow-auto", + "rounded-lg border border-nb-gray-900 bg-nb-gray-935", + "p-2 pr-4", + )} + > + {span} + + ) : ( + span + )} + +
+ ); +}; + +const ResolvedIpsTooltip = ({ ips }: { ips: string[] }) => { + const { t } = useTranslation(); + return ( + <> +
+ {t("networks.ips.heading")} +
+
    + {ips.map((ip) => ( +
  • + + + {ip} + + +
  • + ))} +
+ + ); +}; + +type ToggleProps = { + checked: boolean; + mixed?: boolean; +}; + +const NetworkToggle = ({ checked, mixed }: ToggleProps) => { + const checkedTranslate = checked ? "translate-x-[1.125rem]" : "translate-x-0.5"; + return ( + + + + ); +}; diff --git a/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx b/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx new file mode 100644 index 000000000..0bb9a0c17 --- /dev/null +++ b/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx @@ -0,0 +1,539 @@ +import { + type ComponentType, + type KeyboardEvent as ReactKeyboardEvent, + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { AnimatePresence, motion, type Transition } from "framer-motion"; +import * as Popover from "@radix-ui/react-popover"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { + ArrowDownIcon, + ArrowLeftIcon, + ArrowUpDownIcon, + ArrowUpIcon, + ChevronDownIcon, + ChevronsLeftRightEllipsisIcon, + ClockIcon, + GaugeIcon, + HandshakeIcon, + KeyRoundIcon, + Layers3Icon, + type LucideProps, + MapPinIcon, + MonitorIcon, + Radio, + RefreshCwIcon, + WaypointsIcon, +} from "lucide-react"; +import type { PeerStatus } from "@bindings/services/models.js"; +import { cn } from "@/lib/cn"; +import { CopyToClipboard } from "@/components/CopyToClipboard"; +import { Tooltip } from "@/components/Tooltip"; +import { TruncatedText } from "@/components/TruncatedText"; +import { formatBytes, formatRelative, latencyColor, shortenDns } from "@/lib/formatters"; +import { useStatus } from "@/contexts/StatusContext"; +import { usePeerDetail } from "@/contexts/PeerDetailContext"; +import { peerStatusLabelKey } from "./Peers"; + +const DEFAULT_TRANSITION: Transition = { + duration: 0.32, + ease: [0.32, 0.72, 0, 1], +}; + +const DASH = "-"; + +const dotClass = (connStatus: string): string => { + switch (connStatus) { + case "Connected": + return "bg-green-400"; + case "Connecting": + return "bg-yellow-300 animate-pulse-slow"; + default: + return "bg-nb-gray-500"; + } +}; + +type Props = { + transition?: Transition; +}; + +export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => { + const { t } = useTranslation(); + const { selected, setSelected } = usePeerDetail(); + const { status, refresh } = useStatus(); + + useEffect(() => { + if (!selected) return; + const peers = status?.peers ?? []; + const fresh = peers.find((p) => p.pubKey === selected.pubKey); + if (!fresh) { + setSelected(null); + return; + } + if (fresh !== selected) setSelected(fresh); + }, [status, selected, setSelected]); + + // Daemon updates latency/bytes/handshake without pushing a fresh status + // snapshot, so tick locally to keep relative timestamps live. + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + if (!selected) return; + const id = setInterval(() => setNow(Date.now()), 1000); + return () => clearInterval(id); + }, [selected]); + + const [refreshing, setRefreshing] = useState(false); + const onRefresh = useCallback(async () => { + if (refreshing) return; + setRefreshing(true); + const MIN_SPIN_MS = 600; + const minDelay = new Promise((r) => setTimeout(r, MIN_SPIN_MS)); + try { + await Promise.all([refresh(), minDelay]); + } finally { + setRefreshing(false); + } + }, [refresh, refreshing]); + + useEffect(() => { + if (!selected) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setSelected(null); + return; + } + if (e.key === "ArrowLeft") { + const target = e.target as HTMLElement | null; + const tag = target?.tagName; + if (tag === "INPUT" || tag === "TEXTAREA" || target?.isContentEditable) return; + setSelected(null); + } + }; + globalThis.addEventListener("keydown", onKey); + return () => globalThis.removeEventListener("keydown", onKey); + }, [selected, setSelected]); + + const dialogRef = useRef(null); + const backButtonRef = useRef(null); + + useEffect(() => { + if (!selected) return; + // Defer focus until the slide-in animation has started rendering. + // preventScroll avoids the browser scrolling the parent to chase the + // still-offscreen button, which lands as a stutter at the end of the slide. + requestAnimationFrame(() => backButtonRef.current?.focus({ preventScroll: true })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selected?.pubKey]); + + const getFocusable = (): HTMLElement[] => { + const root = dialogRef.current; + if (!root) return []; + const sel = + "button:not([disabled]), [href], input:not([disabled]), select:not([disabled])," + + ' textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; + return Array.from(root.querySelectorAll(sel)).filter( + (el) => el.offsetParent !== null || el === document.activeElement, + ); + }; + + const onDialogKeyDown = (e: ReactKeyboardEvent) => { + if (e.key !== "Tab") return; + const focusables = getFocusable(); + if (focusables.length === 0) return; + const first = focusables[0]; + const last = focusables[focusables.length - 1]; + const active = document.activeElement as HTMLElement | null; + if (e.shiftKey) { + if (active === first || !active || !dialogRef.current?.contains(active)) { + e.preventDefault(); + last.focus(); + } + } else if (active === last) { + e.preventDefault(); + first.focus(); + } + }; + + return ( + + {selected && ( + +
+ + + + + + + {shortenDns(selected.fqdn) || selected.ip} + + + + + +
+ + + + + + + + +
+ )} +
+ ); +}; + +const PeerDetails = ({ peer, now }: { peer: PeerStatus; now: number }) => { + const { t } = useTranslation(); + const formatAge = (unix: number, fallback: string): string => { + if (!Number.isFinite(unix) || unix <= 0) return fallback; + const diff = Math.floor(now / 1000 - unix); + if (diff < 1) return t("peers.details.justNow"); + return formatRelative(unix, now) ?? fallback; + }; + const lastHandshake = formatAge(peer.lastHandshakeUnix, t("peers.details.never")); + const statusSince = formatAge(peer.connStatusUpdateUnix, DASH); + const isConnected = peer.connStatus === "Connected"; + const connectionLabel = peer.relayed ? t("peers.details.relayed") : t("peers.details.p2p"); + + return ( +
    + + {peer.ip ? ( + + {peer.ip} + + ) : ( + DASH + )} + + {peer.ipv6 && ( + + + + + + )} + {isConnected && ( + + {connectionLabel} + + )} + {peer.relayed && ( + + {peer.relayAddress ? ( + + + + ) : ( + DASH + )} + + )} + {peer.latencyMs > 0 && ( + + + {peer.latencyMs} ms + + + )} + {(peer.bytesRx > 0 || peer.bytesTx > 0) && ( + +
    +
    + + {t("peers.details.bytesReceived")}: + {formatBytes(peer.bytesRx)} +
    +
    + + {t("peers.details.bytesSent")}: + {formatBytes(peer.bytesTx)} +
    +
    +
    + )} + + {lastHandshake} + + + {statusSince} + + {peer.networks.length > 0 && ( + + + + )} + + + + {peer.pubKey ? ( + + + + ) : ( + DASH + )} + +
+ ); +}; + +type RowProps = { + icon: ComponentType; + iconClassName?: string; + label: string; + children: ReactNode; +}; + +type IceRowProps = { + icon: ComponentType; + baseLabel: string; + type: string; + endpoint: string; +}; + +const capitalize = (s: string): string => (s ? s[0].toUpperCase() + s.slice(1) : s); + +const IceRow = ({ icon, baseLabel, type, endpoint }: IceRowProps) => { + if (!type && !endpoint) return null; + const label = type ? `${baseLabel} (${capitalize(type)})` : baseLabel; + return ( + + {endpoint ? ( + + + + ) : ( + {capitalize(type)} + )} + + ); +}; + +const ResourcesValue = ({ networks }: { networks: string[] }) => ( + +); + +const ResourcesPopover = ({ networks }: { networks: string[] }) => { + const [open, setOpen] = useState(false); + + return ( + + + + + + e.preventDefault()} + className={cn( + "z-50 max-h-72 max-w-[18rem] overflow-auto", + "rounded-lg border border-nb-gray-900 bg-nb-gray-935", + "p-2 pr-4 shadow-lg outline-none", + )} + > +
    + {networks.map((n) => ( +
  • + + + {n} + + +
  • + ))} +
+
+
+
+ ); +}; + +const TruncatedRowValue = ({ value, mono }: { value: string; mono?: boolean }) => ( + +); + +const Row = ({ icon: Icon, iconClassName, label, children }: RowProps) => ( +
  • + + {label} + + {children} + +
  • +); diff --git a/client/ui/frontend/src/modules/main/advanced/peers/PeerFilters.tsx b/client/ui/frontend/src/modules/main/advanced/peers/PeerFilters.tsx new file mode 100644 index 000000000..25d9ef822 --- /dev/null +++ b/client/ui/frontend/src/modules/main/advanced/peers/PeerFilters.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { CheckIcon, ChevronDown, ListFilter } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/cn"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/DropdownMenu"; + +export type StatusFilter = "all" | "online" | "offline"; + +type Props = { + value: StatusFilter; + onChange: (value: StatusFilter) => void; + counts: Record; + disabled?: boolean; +}; + +export const PeerFilters = ({ value, onChange, counts, disabled }: Props) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const filters: { value: StatusFilter; label: string }[] = [ + { value: "all", label: t("peers.filter.all") }, + { value: "online", label: t("peers.filter.online") }, + { value: "offline", label: t("peers.filter.offline") }, + ]; + const active = filters.find((f) => f.value === value) ?? filters[0]; + + const handleSelect = (v: StatusFilter) => { + onChange(v); + setOpen(false); + }; + + return ( + + + + + {active.label} ({counts[active.value]}) + + + + + {filters.map((f) => { + const checked = f.value === value; + return ( + handleSelect(f.value)} + role={"menuitemradio"} + aria-checked={checked} + className={"gap-2"} + > + + {f.label}{" "} + ({counts[f.value]}) + + + {checked && } + + + ); + })} + + + ); +}; diff --git a/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx b/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx new file mode 100644 index 000000000..d61221758 --- /dev/null +++ b/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx @@ -0,0 +1,353 @@ +import { type KeyboardEvent, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; +import { ChevronRightIcon, MonitorSmartphoneIcon } from "lucide-react"; +import type { PeerStatus } from "@bindings/services/models.js"; +import { cn } from "@/lib/cn"; +import { reconcileOrder } from "@/lib/sorting"; +import { CopyToClipboard } from "@/components/CopyToClipboard"; +import { SearchInput } from "@/components/inputs/SearchInput"; +import { EmptyState } from "@/components/empty-state/EmptyState"; +import { NoResults } from "@/components/empty-state/NoResults"; +import { latencyColor, shortenDns } from "@/lib/formatters"; +import { useStatus } from "@/contexts/StatusContext"; +import { usePeerDetail } from "@/contexts/PeerDetailContext"; +import { Tooltip } from "@/components/Tooltip"; +import { TruncatedText } from "@/components/TruncatedText"; +import { PeerFilters, type StatusFilter } from "./PeerFilters"; + +const isOnline = (connStatus: string) => connStatus === "Connected"; + +const dotClass = (connStatus: string): string => { + switch (connStatus) { + case "Connected": + return "bg-green-400"; + case "Connecting": + return "bg-yellow-300 animate-pulse-slow"; + default: + return "bg-nb-gray-500"; + } +}; + +export const peerStatusLabelKey = (connStatus: string): string => { + switch (connStatus) { + case "Connected": + return "peers.status.connected"; + case "Connecting": + return "peers.status.connecting"; + default: + return "peers.status.disconnected"; + } +}; + +export const Peers = () => { + const { t } = useTranslation(); + const { status } = useStatus(); + const [search, setSearch] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [scrollParent, setScrollParent] = useState(null); + const searchRef = useRef(null); + + useEffect(() => { + searchRef.current?.focus(); + }, []); + + const isConnected = status?.status === "Connected"; + const peers = useMemo(() => status?.peers ?? [], [status?.peers]); + + const counts = useMemo>(() => { + const online = peers.filter((p) => isOnline(p.connStatus)).length; + return { + all: peers.length, + online, + offline: peers.length - online, + }; + }, [peers]); + + // Stay in live-sort until every peer is stable. Right after Up the daemon + // emits all peers as "Connecting"; committing then would lock that + // alphabetical-only order forever. + const orderRef = useRef([]); + const stickyRef = useRef(false); + const ordered = useMemo(() => { + const compare = (a: PeerStatus, b: PeerStatus) => { + const aOnline = isOnline(a.connStatus); + const bOnline = isOnline(b.connStatus); + if (aOnline !== bOnline) return aOnline ? -1 : 1; + const aName = (a.fqdn || a.ip).toLowerCase(); + const bName = (b.fqdn || b.ip).toLowerCase(); + return aName.localeCompare(bName); + }; + + if (peers.length === 0) { + orderRef.current = []; + stickyRef.current = false; + return []; + } + + if (!stickyRef.current) { + const sorted = [...peers].sort(compare); + if (peers.every((p) => p.connStatus !== "Connecting")) { + orderRef.current = sorted.map((p) => p.pubKey); + stickyRef.current = true; + } + return sorted; + } + + const { order, items } = reconcileOrder(orderRef.current, peers, (p) => p.pubKey, compare); + orderRef.current = order; + return items; + }, [peers]); + + const filtered = useMemo(() => { + const q = search.trim().toLowerCase(); + return ordered.filter((p) => { + if (statusFilter === "online" && !isOnline(p.connStatus)) return false; + if (statusFilter === "offline" && isOnline(p.connStatus)) return false; + return !q || p.fqdn.toLowerCase().includes(q) || p.ip.includes(q); + }); + }, [ordered, search, statusFilter]); + + if (isConnected && peers.length === 0) { + return ( + + ); + } + + return ( +
    +
    +
    + setSearch(e.target.value)} + /> +
    + +
    + {filtered.length === 0 ? ( + + ) : ( + + + {scrollParent && } + + + + + + )} +
    + ); +}; + +const ListTopSpacer = () =>
    ; + +type PeersListProps = { + data: PeerStatus[]; + scrollParent: HTMLElement; +}; + +const PeersList = ({ data, scrollParent }: PeersListProps) => { + const { setSelected } = usePeerDetail(); + const virtuosoRef = useRef(null); + const rowRefs = useRef>(new Map()); + + const focusRow = (index: number) => { + if (index < 0 || index >= data.length) return; + const peer = data[index]; + const tryFocus = () => { + const el = rowRefs.current.get(peer.pubKey); + if (el) { + el.focus(); + return true; + } + return false; + }; + if (!tryFocus()) { + virtuosoRef.current?.scrollToIndex({ index, behavior: "auto" }); + // Row may not be mounted yet — retry after Virtuoso renders it. + requestAnimationFrame(() => { + if (!tryFocus()) requestAnimationFrame(tryFocus); + }); + } + }; + + const handleRowKeyDown = (e: KeyboardEvent, index: number) => { + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + focusRow(Math.min(index + 1, data.length - 1)); + break; + case "ArrowUp": + e.preventDefault(); + focusRow(Math.max(index - 1, 0)); + break; + case "ArrowRight": + e.preventDefault(); + setSelected(data[index]); + break; + case "Home": + e.preventDefault(); + focusRow(0); + break; + case "End": + e.preventDefault(); + focusRow(data.length - 1); + break; + } + }; + + const setRowRef = (pubKey: string, el: HTMLButtonElement | null) => { + if (el) rowRefs.current.set(pubKey, el); + else rowRefs.current.delete(pubKey); + }; + + const ctx = useMemo( + () => ({ onKeyDown: handleRowKeyDown, onSelect: setSelected, setRowRef }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [data, setSelected], + ); + + return ( + + ref={virtuosoRef} + data={data} + customScrollParent={scrollParent} + increaseViewportBy={400} + computeItemKey={(_, peer) => peer.pubKey} + components={{ Header: ListTopSpacer }} + context={ctx} + itemContent={renderPeerRow} + /> + ); +}; + +type PeerRowContext = { + onKeyDown: (e: KeyboardEvent, index: number) => void; + onSelect: (peer: PeerStatus) => void; + setRowRef: (pubKey: string, el: HTMLButtonElement | null) => void; +}; + +const renderPeerRow = (index: number, peer: PeerStatus, ctx: PeerRowContext): ReactNode => ( + +); + +type PeerRowProps = { + peer: PeerStatus; + index: number; + onKeyDown: (e: KeyboardEvent, index: number) => void; + onSelect: (peer: PeerStatus) => void; + setRowRef: (pubKey: string, el: HTMLButtonElement | null) => void; +}; + +const PeerRow = ({ peer, index, onKeyDown, onSelect, setRowRef }: PeerRowProps) => { + const { t } = useTranslation(); + const isConnected = peer.connStatus === "Connected"; + const peerName = shortenDns(peer.fqdn) || peer.ip; + const statusLabel = t(peerStatusLabelKey(peer.connStatus)); + const handleKey = (e: KeyboardEvent) => onKeyDown(e, index); + return ( +
    +
    + ); +}; diff --git a/client/ui/frontend/src/modules/profiles/ProfileAvatar.tsx b/client/ui/frontend/src/modules/profiles/ProfileAvatar.tsx new file mode 100644 index 000000000..70d45acc5 --- /dev/null +++ b/client/ui/frontend/src/modules/profiles/ProfileAvatar.tsx @@ -0,0 +1,76 @@ +import { type ButtonHTMLAttributes, forwardRef } from "react"; +import { + Briefcase, + Building, + Cloud, + Construction, + FlaskConical, + Gamepad2, + GraduationCap, + House, + Radio, + Server, + SquareCode, + Terminal, + UserCircle, + UserPlus, + Users, + type LucideIcon, +} from "lucide-react"; +import { cn } from "@/lib/cn"; + +// Scanned in order — put more-specific tokens first (e.g. "staging" before "stage"). +const ICON_MAP: ReadonlyArray<[RegExp, LucideIcon]> = [ + [/(default|personal)/i, UserCircle], + [/(work|business|office|company|corp|corporate)/i, Briefcase], + [/(home|house|private)/i, House], + [/(dev|development|developer|code|coding|engineering)/i, SquareCode], + [/(local|localhost|loopback)/i, Terminal], + [/(stage|staging|preprod|pre-prod)/i, Construction], + [/(test|testing|qa)/i, FlaskConical], + [/(prod|production)/i, Cloud], + [/(live)/i, Radio], + [/(selfhosted|self-hosted|on-prem|onprem)/i, Server], + [/(school|university|edu|study|student)/i, GraduationCap], + [/(client|customer)/i, Building], + [/(family)/i, Users], + [/(gaming|game)/i, Gamepad2], + [/(guest)/i, UserPlus], +]; + +export const pickProfileIcon = (name: string | undefined): LucideIcon | null => { + if (!name) return null; + for (const [pattern, Icon] of ICON_MAP) { + if (pattern.test(name)) return Icon; + } + return null; +}; + +type Props = ButtonHTMLAttributes & { + name?: string; + size?: number; +}; + +export const ProfileAvatar = forwardRef(function ProfileAvatar( + { name = "", size = 28, className, type = "button", ...props }, + ref, +) { + const Icon = pickProfileIcon(name) ?? UserCircle; + return ( + + ); +}); diff --git a/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx b/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx new file mode 100644 index 000000000..19313ccb3 --- /dev/null +++ b/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx @@ -0,0 +1,263 @@ +import { type FormEvent, useEffect, useId, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import * as Dialog from "@/components/dialog/Dialog"; +import { Input } from "@/components/inputs/Input"; +import { Button } from "@/components/buttons/Button"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { Label } from "@/components/typography/Label"; +import { HelpText } from "@/components/typography/HelpText"; +import { ManagementServerSwitch } from "@/components/ManagementServerSwitch"; +import { + CLOUD_MANAGEMENT_URL, + ManagementMode, + checkManagementUrlReachable, + isValidManagementUrl, + normalizeManagementUrl, +} from "@/hooks/useManagementUrl"; +import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; + +export type ProfileFormInitial = { + name: string; + managementUrl: string; +}; + +type Props = { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (name: string, managementUrl: string) => void | Promise; + initial?: ProfileFormInitial; +}; + +const MAX_PROFILE_NAME_LEN = 128; + +export const ProfileCreationModal = ({ open, onOpenChange, onSubmit, initial }: Props) => { + const { t } = useTranslation(); + const { mdm } = useRestrictions(); + const managedManagementUrl = mdm.managementURL; + const nameId = useId(); + const urlId = useId(); + const isEdit = !!initial; + const initialModeFromUrl = (u: string): ManagementMode => + u && u !== CLOUD_MANAGEMENT_URL ? ManagementMode.SelfHosted : ManagementMode.Cloud; + const initialSelfHostedUrl = (u: string): string => (u && u !== CLOUD_MANAGEMENT_URL ? u : ""); + + const [name, setName] = useState(initial?.name ?? ""); + const [nameError, setNameError] = useState(null); + const nameRef = useRef(null); + + const [mode, setMode] = useState( + initial ? initialModeFromUrl(initial.managementUrl) : ManagementMode.Cloud, + ); + const [url, setUrl] = useState(initial ? initialSelfHostedUrl(initial.managementUrl) : ""); + const [urlError, setUrlError] = useState(null); + const [unreachable, setUnreachable] = useState(false); + const [checking, setChecking] = useState(false); + const urlRef = useRef(null); + + useEffect(() => { + if (open) { + setName(initial?.name ?? ""); + setMode(initial ? initialModeFromUrl(initial.managementUrl) : ManagementMode.Cloud); + setUrl(initial ? initialSelfHostedUrl(initial.managementUrl) : ""); + setNameError(null); + setUrlError(null); + setUnreachable(false); + setChecking(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, initial?.name, initial?.managementUrl]); + + const initialModeRef = useRef(ManagementMode.Cloud); + useEffect(() => { + if (!open) return; + initialModeRef.current = mode; + const id = globalThis.setTimeout(() => { + nameRef.current?.focus(); + nameRef.current?.select(); + }, 0); + return () => globalThis.clearTimeout(id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + + // When the user toggles to Self-hosted inside the dialog (not on initial + // open), move focus to the URL input so they can start typing immediately. + useEffect(() => { + if (!open) return; + if (mode === initialModeRef.current) return; + if (mode !== ManagementMode.SelfHosted) return; + urlRef.current?.focus(); + }, [open, mode]); + + useEffect(() => { + setUrlError(null); + setUnreachable(false); + }, [url, mode]); + + const resolveTargetUrl = (): { url: string; needsReachCheck: boolean } | null => { + if (managedManagementUrl) { + return { url: managedManagementUrl, needsReachCheck: false }; + } + if (mode === ManagementMode.Cloud) { + return { url: CLOUD_MANAGEMENT_URL, needsReachCheck: false }; + } + const trimmed = url.trim(); + if (!trimmed || !isValidManagementUrl(trimmed)) { + setUrlError(t("settings.general.management.urlError")); + urlRef.current?.focus(); + return null; + } + const target = normalizeManagementUrl(trimmed); + + const unchanged = target === initial?.managementUrl; + return { url: target, needsReachCheck: !unchanged }; + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (checking) return; + + const sanitized = name.trim(); + if (sanitized.length === 0) { + setNameError(t("profile.dialog.required")); + nameRef.current?.focus(); + return; + } + + const target = resolveTargetUrl(); + if (!target) return; + + if (target.needsReachCheck) { + setChecking(true); + const reachable = await checkManagementUrlReachable(target.url); + setChecking(false); + if (!reachable && !unreachable) { + setUnreachable(true); + return; + } + } + + await onSubmit(sanitized, target.url); + onOpenChange(false); + }; + + const handleNameChange = (value: string) => { + setName(value); + if (nameError) setNameError(null); + }; + + const trimmedUrl = url.trim(); + const showUrlSyntaxError = + mode === ManagementMode.SelfHosted && + trimmedUrl !== "" && + !isValidManagementUrl(trimmedUrl); + const urlInputError = showUrlSyntaxError + ? t("settings.general.management.urlError") + : (urlError ?? undefined); + const urlInputWarning = + !urlInputError && unreachable ? t("profile.dialog.urlUnreachable") : undefined; + + return ( + + { + e.preventDefault(); + // Focus + select-all so editing an existing name is one + // keystroke away from overwriting it. + nameRef.current?.focus(); + nameRef.current?.select(); + }} + > +
    +
    +
    +
    + + + {t("profile.dialog.description")} + +
    + handleNameChange(e.target.value)} + error={nameError ?? undefined} + maxLength={MAX_PROFILE_NAME_LEN} + spellCheck={false} + autoComplete={"off"} + autoCapitalize={"off"} + /> +
    + + {!managedManagementUrl && ( +
    +
    + + + {t("profile.dialog.managementHelp")} + +
    +
    + + {mode === ManagementMode.SelfHosted && ( + setUrl(e.target.value)} + error={urlInputError} + warning={urlInputWarning} + spellCheck={false} + autoComplete={"off"} + autoCorrect={"off"} + autoCapitalize={"off"} + /> + )} +
    +
    + )} + + + + + +
    +
    +
    +
    + ); +}; diff --git a/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx b/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx new file mode 100644 index 000000000..e76e300c1 --- /dev/null +++ b/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx @@ -0,0 +1,293 @@ +import { forwardRef, useLayoutEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import * as Popover from "@radix-ui/react-popover"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { Command } from "cmdk"; +import { Check, ChevronDown, Settings2, UserCircle } from "lucide-react"; +import { pickProfileIcon } from "@/modules/profiles/ProfileAvatar"; +import type { Profile } from "@bindings/services/models.js"; +import { Tooltip } from "@/components/Tooltip"; +import { useProfile } from "@/contexts/ProfileContext"; +import { useFocusVisible } from "@/hooks/useFocusVisible"; +import { cn } from "@/lib/cn"; +import { errorDialog, formatErrorMessage } from "@/lib/errors"; + +type ProfileDropdownProps = { + onManageProfiles?: () => void; +}; + +const MANAGE_VALUE = "__manage_profiles__"; + +export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { + const { t } = useTranslation(); + const { activeProfile, activeProfileId, profiles, switchProfile, loaded } = useProfile(); + const [open, setOpen] = useState(false); + const [busy, setBusy] = useState(false); + const listRef = useRef(null); + + const handleTriggerKeyDown = (e: React.KeyboardEvent) => { + if (open) return; + if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + setOpen(true); + } + }; + + const sortedProfiles = [...profiles].sort((a, b) => { + if (a.id === activeProfileId) return -1; + if (b.id === activeProfileId) return 1; + return a.name.localeCompare(b.name); + }); + + const guarded = async (title: string, fn: () => Promise) => { + if (busy) return; + setBusy(true); + try { + await fn(); + } catch (e) { + await errorDialog({ + Title: title, + Message: formatErrorMessage(e), + }); + } finally { + setBusy(false); + } + }; + + const handleSelect = (id: string) => { + setOpen(false); + if (id === activeProfileId) return; + void guarded(t("profile.error.switchTitle"), () => switchProfile(id)); + }; + + const handleManage = () => { + setOpen(false); + onManageProfiles?.(); + }; + + if (!loaded) return ; + + const hasProfile = !!activeProfileId; + const activeFromList = profiles.find((p) => p.id === activeProfileId)?.name; + const displayName = hasProfile + ? (activeFromList ?? activeProfile) + : t("profile.selector.noProfile"); + + return ( + + + + + + { + e.preventDefault(); + listRef.current?.focus(); + }} + className={cn( + "wails-no-draggable z-50 min-w-64 select-none overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg", + "data-[state=open]:animate-in data-[state=closed]:animate-out", + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", + "data-[side=bottom]:origin-top data-[side=top]:origin-bottom", + "data-[side=left]:origin-right data-[side=right]:origin-left", + "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2", + "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + )} + > + e.stopPropagation()} + className={"outline-none focus:outline-none focus-visible:outline-none"} + > + + {sortedProfiles.length > 0 && ( + <> + + + {sortedProfiles.map((profile) => ( + + ))} + + + + + +
    + + )} + +
    + + + + {t("profile.dropdown.manageProfiles")} + + +
    + + + + + + ); +}; + +const ProfileTriggerSkeleton = () => ( +
    +
    +
    +
    +); + +type ProfileTriggerButtonProps = React.ButtonHTMLAttributes & { + name: string; +}; + +const ProfileTriggerButton = forwardRef( + function ProfileTriggerButton({ name, className, disabled, ...props }, ref) { + const { t } = useTranslation(); + const isFocusVisible = useFocusVisible(); + const Icon = pickProfileIcon(name) ?? UserCircle; + return ( + + ); + }, +); + +type ProfileRowProps = { + profile: Profile; + isActive: boolean; + onSelect: (id: string) => void; +}; + +const ProfileRow = ({ profile, isActive, onSelect }: ProfileRowProps) => { + const showEmail = !!profile.email; + return ( + onSelect(profile.id)} + className={cn( + "flex w-auto gap-2 px-2 py-2 pr-3 last:mb-1", + "cursor-default rounded-md text-sm outline-none", + "data-[selected=true]:bg-nb-gray-900", + showEmail ? "items-start" : "items-center", + )} + > +
    + {profile.name} + {showEmail && } +
    + {isActive && ( + + )} +
    + ); +}; + +const TruncatedEmail = ({ email }: { email: string }) => { + const ref = useRef(null); + const [overflowing, setOverflowing] = useState(false); + + useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + setOverflowing(el.scrollWidth > el.clientWidth); + }, [email]); + + const span = ( + + {email} + + ); + if (!overflowing) return span; + return {span}; +}; diff --git a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx new file mode 100644 index 000000000..a602e94bf --- /dev/null +++ b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx @@ -0,0 +1,679 @@ +import { type KeyboardEvent, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + CircleMinus, + LogIn, + MoreVertical, + PencilLine, + PlusCircle, + Trash2, + UserCircle, +} from "lucide-react"; +import type { Profile } from "@bindings/services/models.js"; +import { Badge } from "@/components/Badge"; +import { Button } from "@/components/buttons/Button"; +import HelpText from "@/components/typography/HelpText"; +import { + ProfileCreationModal, + type ProfileFormInitial, +} from "@/modules/profiles/ProfileCreationModal"; +import { pickProfileIcon } from "@/modules/profiles/ProfileAvatar"; +import { Tooltip } from "@/components/Tooltip"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/DropdownMenu"; +import i18next from "@/lib/i18n"; +import { useProfile } from "@/contexts/ProfileContext"; +import { useConfirm } from "@/contexts/DialogContext"; +import { Settings as SettingsSvc } from "@bindings/services"; +import { SetConfigParams } from "@bindings/services/models.js"; +import { isNetbirdCloud } from "@/hooks/useManagementUrl.ts"; +import { SectionGroup, SettingsBottomBar } from "@/modules/settings/SettingsSection.tsx"; +import { cn } from "@/lib/cn"; +import { reconcileOrder } from "@/lib/sorting"; +import { errorDialog, formatErrorMessage } from "@/lib/errors"; + +const DEFAULT_PROFILE = "default"; + +export function ProfilesTab() { + const { t } = useTranslation(); + const { + profiles, + activeProfileId, + loaded, + username, + switchProfile, + addProfile, + removeProfile, + renameProfile, + logoutProfile, + } = useProfile(); + + const confirm = useConfirm(); + const [newOpen, setNewOpen] = useState(false); + const [editTarget, setEditTarget] = useState<{ + profile: Profile; + initial: ProfileFormInitial; + } | null>(null); + const [busy, setBusy] = useState(false); + + // Order is held stable so switching only flips the badge, never reorders rows + // (else the clicked row jumps to the top under the cursor). + const orderRef = useRef([]); + const ordered = useMemo(() => { + const { order, items } = reconcileOrder( + orderRef.current, + profiles, + (p) => p.id, + (a, b) => { + if (a.id === activeProfileId) return -1; + if (b.id === activeProfileId) return 1; + return a.name.localeCompare(b.name); + }, + ); + orderRef.current = order; + return items; + }, [profiles, activeProfileId]); + + const guarded = async (title: string, fn: () => Promise) => { + if (busy) return; + setBusy(true); + try { + await fn(); + } catch (e) { + await errorDialog({ + Title: title, + Message: formatErrorMessage(e), + }); + } finally { + setBusy(false); + } + }; + + const handleSwitch = async (id: string, name: string) => { + const ok = await confirm({ + title: t("profile.switch.title", { name }), + description: t("profile.switch.message", { name }), + confirmLabel: t("profile.switch.confirm"), + }); + if (!ok) return; + await guarded(i18next.t("profile.error.switchTitle"), () => switchProfile(id)); + }; + + const handleDeregister = async (id: string, name: string) => { + const ok = await confirm({ + title: t("profile.deregister.title", { name }), + description: t("profile.deregister.message", { name }), + confirmLabel: t("profile.deregister.confirm"), + }); + if (!ok) return; + void guarded(i18next.t("profile.error.deregisterTitle"), () => logoutProfile(id)); + }; + + const handleDelete = async (id: string, name: string) => { + if (name === DEFAULT_PROFILE) return; + const ok = await confirm({ + title: t("profile.delete.title", { name }), + description: t("profile.delete.message", { name }), + confirmLabel: t("common.delete"), + danger: true, + }); + if (!ok) return; + void guarded(i18next.t("profile.error.deleteTitle"), () => removeProfile(id)); + }; + + const handleCreate = async (name: string, managementUrl: string) => { + await guarded(i18next.t("profile.error.createTitle"), async () => { + const id = await addProfile(name); + // SetConfig is keyed by the new profile's ID, so it writes the + // not-yet-active profile. Write before switching so any reconnect + // targets the right deployment. + if (!isNetbirdCloud(managementUrl)) { + await SettingsSvc.SetConfig( + new SetConfigParams({ profileName: id, username, managementUrl }), + ); + } + await switchProfile(id); + }); + }; + + const handleEdit = async (id: string, name: string) => { + await guarded(i18next.t("profile.error.editTitle"), async () => { + const config = await SettingsSvc.GetConfig({ profileName: id, username }); + const profile = profiles.find((p) => p.id === id); + if (!profile) return; + setEditTarget({ + profile, + initial: { name, managementUrl: config.managementUrl }, + }); + }); + }; + + const handleSave = async (name: string, managementUrl: string) => { + if (!editTarget) return; + const { profile, initial } = editTarget; + await guarded(i18next.t("profile.error.editTitle"), async () => { + if (name !== initial.name) { + await renameProfile(profile.id, name); + } + if (managementUrl !== initial.managementUrl) { + await SettingsSvc.SetConfig( + new SetConfigParams({ + profileName: profile.id, + username, + managementUrl, + }), + ); + } + }); + }; + + return ( +
    + + {t("settings.profiles.intro")} + +
    + + + {loaded && ordered.length === 0 && ( +
    + +

    + {t("settings.profiles.emptyTitle")} +

    +

    + {t("settings.profiles.emptyDescription")} +

    +
    + )} +
    + + + + +
    + + + + { + if (!o) setEditTarget(null); + }} + initial={editTarget?.initial} + onSubmit={handleSave} + /> +
    + ); +} + +type ProfilesTableProps = { + ordered: Profile[]; + activeProfileId: string | undefined; + onSwitch: (id: string, name: string) => void; + onEdit: (id: string, name: string) => void; + onDeregister: (id: string, name: string) => void; + onDelete: (id: string, name: string) => void; +}; + +const ProfilesTable = ({ + ordered, + activeProfileId, + onSwitch, + onEdit, + onDeregister, + onDelete, +}: ProfilesTableProps) => { + const { t } = useTranslation(); + const [focusedIndex, setFocusedIndex] = useState(0); + const rowRefs = useRef>(new Map()); + + const focusRow = (index: number) => { + if (index < 0 || index >= ordered.length) return; + setFocusedIndex(index); + const el = rowRefs.current.get(ordered[index].id); + el?.focus(); + }; + + const actionButtonsIn = (row: HTMLTableRowElement | undefined) => + Array.from( + row?.querySelectorAll( + "button:not([aria-hidden='true']):not([aria-disabled='true'])", + ) ?? [], + ); + + const handleRowKey = (e: KeyboardEvent, index: number): boolean => { + switch (e.key) { + case "ArrowDown": + focusRow(Math.min(index + 1, ordered.length - 1)); + return true; + case "ArrowUp": + focusRow(Math.max(index - 1, 0)); + return true; + case "Home": + focusRow(0); + return true; + case "End": + focusRow(ordered.length - 1); + return true; + } + return false; + }; + + const handleButtonKey = ( + e: KeyboardEvent, + index: number, + row: HTMLTableRowElement, + ): boolean => { + const buttons = actionButtonsIn(row); + const current = buttons.indexOf(e.target as HTMLButtonElement); + if (current === -1) return false; + + switch (e.key) { + case "ArrowDown": + focusRow(Math.min(index + 1, ordered.length - 1)); + return true; + case "ArrowUp": + focusRow(Math.max(index - 1, 0)); + return true; + case "Escape": + row.focus(); + return true; + case "Tab": + // At the last button: jump to the next row instead of exiting the table. + // At the first button with Shift+Tab: jump back to the row. + if (!e.shiftKey && current === buttons.length - 1 && index < ordered.length - 1) { + focusRow(index + 1); + return true; + } + if (e.shiftKey && current === 0) { + row.focus(); + return true; + } + return false; + } + return false; + }; + + const handleRowKeyDown = (e: KeyboardEvent, index: number) => { + const row = rowRefs.current.get(ordered[index].id); + if (!row) return; + const onRow = e.target === row; + const handled = onRow ? handleRowKey(e, index) : handleButtonKey(e, index, row); + if (handled) e.preventDefault(); + }; + + const safeFocusedIndex = Math.min(focusedIndex, Math.max(0, ordered.length - 1)); + + return ( + + + {ordered.map((profile, index) => ( + { + if (el) rowRefs.current.set(profile.id, el); + else rowRefs.current.delete(profile.id); + }} + onKeyDown={(e) => handleRowKeyDown(e, index)} + onFocus={() => setFocusedIndex(index)} + onSwitch={() => onSwitch(profile.id, profile.name)} + onEdit={() => onEdit(profile.id, profile.name)} + onDeregister={() => onDeregister(profile.id, profile.name)} + onDelete={() => onDelete(profile.id, profile.name)} + /> + ))} + +
    + ); +}; + +type ProfileRowProps = { + profile: Profile; + isActive: boolean; + isFocused: boolean; + isFirst: boolean; + isLast: boolean; + rowRef: (el: HTMLTableRowElement | null) => void; + onKeyDown: (e: KeyboardEvent) => void; + onFocus: () => void; + onSwitch: () => void; + onEdit: () => void; + onDeregister: () => void; + onDelete: () => void; +}; + +const ProfileRow = ({ + profile, + isActive, + isFocused, + isFirst, + isLast, + rowRef, + onKeyDown, + onFocus, + onSwitch, + onEdit, + onDeregister, + onDelete, +}: ProfileRowProps) => { + const { t } = useTranslation(); + const Icon = pickProfileIcon(profile.name) ?? UserCircle; + const showEmail = !!profile.email; + + return ( + + + +
    +
    + + {profile.name} + + {isActive && {t("settings.profiles.active")}} +
    + {showEmail && } +
    + + + + + + ); +}; + +const TruncatedEmail = ({ email }: { email: string }) => { + const ref = useRef(null); + const [overflowing, setOverflowing] = useState(false); + + useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + setOverflowing(el.scrollWidth > el.clientWidth); + }, [email]); + + const span = ( + + {email} + + ); + if (!overflowing) return span; + return {span}; +}; + +type RowActionsProps = { + canSwitch: boolean; + canDeregister: boolean; + isDefault: boolean; + isActive: boolean; + rowFocused: boolean; + onSwitch: () => void; + onEdit: () => void; + onDeregister: () => void; + onDelete: () => void; +}; + +const RowActions = ({ + canSwitch, + canDeregister, + isDefault, + isActive, + rowFocused, + onSwitch, + onEdit, + onDeregister, + onDelete, +}: RowActionsProps) => { + const { t } = useTranslation(); + const deleteDisabled = isDefault || isActive; + let deleteDisabledReason: string | null = null; + if (isDefault) deleteDisabledReason = t("profile.delete.disabledDefault"); + else if (isActive) deleteDisabledReason = t("profile.delete.disabledActive"); + return ( +
    +
    + ); +}; + +type RowMoreMenuProps = { + canDeregister: boolean; + deleteDisabled: boolean; + deleteDisabledReason: string | null; + rowFocused: boolean; + onEdit: () => void; + onDeregister: () => void; + onDelete: () => void; +}; + +const RowMoreMenu = ({ + canDeregister, + deleteDisabled, + deleteDisabledReason, + rowFocused, + onEdit, + onDeregister, + onDelete, +}: RowMoreMenuProps) => { + const { t } = useTranslation(); + const moreLabel = t("profile.selector.moreOptions"); + return ( + + + + + + +
    + + {t("profile.selector.edit")} +
    +
    + {canDeregister && ( + +
    + + {t("profile.selector.deregister")} +
    +
    + )} + +
    +
    + ); +}; + +type DeleteMenuItemProps = { + disabled: boolean; + disabledReason: string | null; + onDelete: () => void; +}; + +const DeleteMenuItem = ({ disabled, disabledReason, onDelete }: DeleteMenuItemProps) => { + const { t } = useTranslation(); + const item = ( + +
    + + {t("profile.selector.delete")} +
    +
    + ); + if (!disabled || !disabledReason) return item; + return ( + {disabledReason}} + side={"left"} + > + {item} + + ); +}; + +type ActionIconButtonProps = { + label: string; + icon: typeof CircleMinus; + onClick: () => void; + variant?: "default" | "danger"; + /** Occupies space but invisible and non-interactive (preserves row layout). */ + hidden?: boolean; + disabled?: boolean; + tabbable?: boolean; +}; + +const ActionIconButton = ({ + label, + icon: Icon, + onClick, + variant = "default", + hidden = false, + disabled = false, + tabbable = true, +}: ActionIconButtonProps) => { + const button = ( + + ); + if (hidden) return button; + return ( + {label}} + side={"top"} + > + {button} + + ); +}; diff --git a/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx b/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx new file mode 100644 index 000000000..2ceb958d4 --- /dev/null +++ b/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx @@ -0,0 +1,202 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSearchParams } from "react-router-dom"; +import { Events } from "@wailsio/runtime"; +import { AlertCircleIcon, ClockIcon } from "lucide-react"; +import { Button } from "@/components/buttons/Button"; +import { ConfirmDialog } from "@/components/dialog/ConfirmDialog"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import { SquareIcon } from "@/components/SquareIcon"; +import { Connection, Profiles as ProfilesSvc, Session, WindowManager } from "@bindings/services"; +import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow"; +import { EVENT_BROWSER_LOGIN_CANCEL } from "@/lib/connection"; +import { errorDialog, formatErrorMessage } from "@/lib/errors.ts"; +import { formatRemaining } from "@/lib/formatters"; + +const DEFAULT_SECONDS = 360; +const WINDOW_WIDTH = 360; +const SOON_THRESHOLD_SECONDS = 60 * 60; + +export default function SessionExpirationDialog() { + const { t } = useTranslation(); + const contentRef = useAutoSizeWindow(WINDOW_WIDTH); + const [params] = useSearchParams(); + const initialSeconds = useMemo(() => { + const raw = params.get("seconds"); + if (!raw) return DEFAULT_SECONDS; + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : DEFAULT_SECONDS; + }, [params]); + + const [remaining, setRemaining] = useState(initialSeconds); + const [busy, setBusy] = useState(false); + const busyRef = useRef(busy); + busyRef.current = busy; + const expired = remaining <= 0; + const expiredRef = useRef(expired); + expiredRef.current = expired; + const soon = remaining <= SOON_THRESHOLD_SECONDS; + const activeTitle = soon ? t("sessionExpiration.title") : t("sessionExpiration.titleLater"); + const activeDescription = soon + ? t("sessionExpiration.description") + : t("sessionExpiration.descriptionLater"); + + useEffect(() => { + setRemaining(initialSeconds); + }, [initialSeconds]); + + useEffect(() => { + const id = globalThis.setInterval(() => { + setRemaining((s) => (s <= 1 ? 0 : s - 1)); + }, 1000); + return () => globalThis.clearInterval(id); + }, [initialSeconds]); + + // 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); + } + }); + return () => { + off(); + }; + }, []); + + const stay = useCallback(async () => { + if (busy) return; + setBusy(true); + + let offCancel: (() => void) | undefined; + + try { + const start = await Session.RequestExtend({ hint: "" }); + const uri = start.verificationUriComplete || start.verificationUri; + + // The popup opens the URL and (Go-side) hides this window, restoring it on close. + if (uri) { + try { + await WindowManager.OpenBrowserLogin(uri); + } catch (e) { + console.error(e); + } + } + + const cancelPromise = new Promise((resolve) => { + offCancel = Events.On(EVENT_BROWSER_LOGIN_CANCEL, () => { + resolve(); + }); + }); + + const waitPromise = Session.WaitExtend({ + deviceCode: start.deviceCode, + userCode: start.userCode, + }); + + const outcome = await Promise.race([ + waitPromise.then((r) => ({ kind: "done" as const, result: r })), + cancelPromise.then(() => ({ kind: "cancel" as const })), + ]); + + if (outcome.kind === "cancel") { + waitPromise.cancel?.(); + waitPromise.catch(() => {}); + return; + } + + // Another surface owns this flow; keep the dialog open to retry. + if (outcome.result.preempted) { + return; + } + + // Close before the popup so the restore can't flash this window back. + WindowManager.CloseSessionExpiration().catch(console.error); + } catch (e) { + await errorDialog({ + Title: t("sessionExpiration.extendFailedTitle"), + Message: formatErrorMessage(e), + }); + } finally { + offCancel?.(); + WindowManager.CloseBrowserLogin().catch(console.error); + setBusy(false); + } + }, [busy, t]); + + const logout = useCallback(async () => { + if (busy) return; + setBusy(true); + try { + const username = await ProfilesSvc.Username(); + const active = await ProfilesSvc.GetActive(); + await Connection.Logout({ + profileName: active.id || "default", + username, + }); + WindowManager.CloseSessionExpiration().catch(console.error); + } catch (e) { + await errorDialog({ + Title: t("sessionExpiration.logoutFailedTitle"), + Message: formatErrorMessage(e), + }); + } finally { + setBusy(false); + } + }, [busy, t]); + + const close = useCallback(() => { + WindowManager.CloseSessionExpiration().catch(console.error); + }, []); + + return ( + + + +
    + + {expired ? t("sessionExpiration.expired") : activeTitle} + + + {expired ? t("sessionExpiration.expiredDescription") : activeDescription} + +
    + + {!expired && ( +
    + {formatRemaining(remaining)} +
    + )} + + + + + +
    + ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsAbout.tsx b/client/ui/frontend/src/modules/settings/SettingsAbout.tsx new file mode 100644 index 000000000..8ba1009ba --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsAbout.tsx @@ -0,0 +1,169 @@ +import type { ComponentType, SVGProps } from "react"; +import { useTranslation } from "react-i18next"; +import { Browser } from "@wailsio/runtime"; +import { BookOpen, MessageSquareText, MessagesSquare } from "lucide-react"; +import netbirdFull from "@/assets/logos/netbird-full.svg"; + +// Brand glyphs from simpleicons.org (lucide deprecated its brand icons). +const GithubIcon = (props: SVGProps) => ( + + + +); +const SlackIcon = (props: SVGProps) => ( + + + +); +import { useSettings } from "@/contexts/SettingsContext.tsx"; +import { useStatus } from "@/contexts/StatusContext.tsx"; +import { UpdateVersionCard } from "@/modules/auto-update/UpdateVersionCard"; +import { useAccentTrigger } from "@/modules/settings/SettingsAccent"; + +function openUrl(url: string) { + Browser.OpenURL(url).catch(() => { + window.open(url, "_blank"); + }); +} + +export function SettingsAbout() { + const { t } = useTranslation(); + const { status } = useStatus(); + const { guiVersion } = useSettings(); + const daemonVersion = status?.daemonVersion ?? "—"; + + const handleVersionClick = useAccentTrigger(); + + const COMMUNITY_LINKS: { + label: string; + url: string; + Icon: ComponentType>; + iconClassName?: string; + }[] = [ + { + label: t("settings.about.community.github"), + url: "https://github.com/netbirdio/netbird", + Icon: GithubIcon, + iconClassName: "h-3 w-3", + }, + { + label: t("settings.about.community.slack"), + url: "https://docs.netbird.io/slack-url", + Icon: SlackIcon, + iconClassName: "h-3 w-3", + }, + { + label: t("settings.about.community.forum"), + url: "https://forum.netbird.io", + Icon: MessagesSquare, + }, + { + label: t("settings.about.community.documentation"), + url: "https://docs.netbird.io", + Icon: BookOpen, + }, + { + label: t("settings.about.community.feedback"), + url: "https://forms.gle/TeLw2zrXEdw6RcQ36", + Icon: MessageSquareText, + }, + ]; + + const LEGAL_LINKS: { label: string; url: string }[] = [ + { label: t("settings.about.links.imprint"), url: "https://netbird.io/imprint" }, + { label: t("settings.about.links.privacy"), url: "https://netbird.io/privacy" }, + { label: t("settings.about.links.cla"), url: "https://netbird.io/cla" }, + { label: t("settings.about.links.terms"), url: "https://netbird.io/terms" }, + ]; + + return ( +
    + {t("common.netbird")} +
    + +

    + {guiVersion === "development" ? ( + + {t("settings.about.guiName")}{" "} + + {t("settings.about.development")} + + + ) : ( + t("settings.about.gui", { version: guiVersion }) + )} +

    +
    + + + +

    + {t("settings.about.copyright", { year: new Date().getFullYear() })} +

    +
    + {COMMUNITY_LINKS.map(({ label, url, Icon, iconClassName }) => ( + + ))} +
    +
    + {LEGAL_LINKS.map((link) => ( + + ))} +
    +
    + ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsAccent.tsx b/client/ui/frontend/src/modules/settings/SettingsAccent.tsx new file mode 100644 index 000000000..b7d9c96c8 --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsAccent.tsx @@ -0,0 +1,116 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { createRoot } from "react-dom/client"; + +export function useAccentTrigger() { + const clicksRef = useRef(0); + const lastClickRef = useRef(0); + + return useCallback(() => { + const now = performance.now(); + if (now - lastClickRef.current > 400) { + clicksRef.current = 0; + } + lastClickRef.current = now; + clicksRef.current += 1; + if (clicksRef.current >= 10) { + clicksRef.current = 0; + triggerAccent(); + } + }, []); +} + +function triggerAccent() { + if (document.getElementById("nb-accent-root")) return; + + const container = document.createElement("div"); + container.id = "nb-accent-root"; + document.body.appendChild(container); + const root = createRoot(container); + + const cleanup = () => { + root.unmount(); + container.remove(); + }; + + root.render(); +} + +function Accent({ onDone }: Readonly<{ onDone: () => void }>) { + const canvasRef = useRef(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const raf = requestAnimationFrame(() => setVisible(true)); + return () => cancelAnimationFrame(raf); + }, []); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const resize = () => { + canvas.width = window.innerWidth * dpr; + canvas.height = window.innerHeight * dpr; + canvas.style.width = `${window.innerWidth}px`; + canvas.style.height = `${window.innerHeight}px`; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + }; + resize(); + window.addEventListener("resize", resize); + + const chars = "TEAMNETBIRD"; + const fontSize = 16; + const columns = Math.floor(window.innerWidth / fontSize); + const drops = Array.from({ length: columns }, () => Math.random() * -50); + + let raf = 0; + let last = 0; + const draw = (t: number) => { + if (t - last > 50) { + last = t; + + ctx.globalCompositeOperation = "destination-out"; + ctx.fillStyle = "rgba(0, 0, 0, 0.12)"; + ctx.fillRect(0, 0, window.innerWidth, window.innerHeight); + + ctx.globalCompositeOperation = "source-over"; + ctx.font = `${fontSize}px ui-monospace, monospace`; + ctx.fillStyle = "#f68330"; + + for (let i = 0; i < drops.length; i++) { + const ch = chars[Math.floor(Math.random() * chars.length)]; + const y = drops[i] * fontSize; + ctx.fillText(ch, i * fontSize, y); + if (y > window.innerHeight && Math.random() > 0.975) { + drops[i] = 0; + } + drops[i]++; + } + } + raf = requestAnimationFrame(draw); + }; + raf = requestAnimationFrame(draw); + + const timeout = globalThis.setTimeout(() => { + setVisible(false); + globalThis.setTimeout(onDone, 500); + }, 9000); + + return () => { + cancelAnimationFrame(raf); + globalThis.clearTimeout(timeout); + window.removeEventListener("resize", resize); + }; + }, [onDone]); + + return ( +
    + +
    + ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsAdvanced.tsx b/client/ui/frontend/src/modules/settings/SettingsAdvanced.tsx new file mode 100644 index 000000000..37b1932d9 --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsAdvanced.tsx @@ -0,0 +1,179 @@ +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { System } from "@wailsio/runtime"; +import Button from "@/components/buttons/Button"; +import { HelpText } from "@/components/typography/HelpText"; +import { Input } from "@/components/inputs/Input"; +import { Label } from "@/components/typography/Label"; +import { SectionGroup, SettingsBottomBar } from "@/modules/settings/SettingsSection.tsx"; +import { useSettings } from "@/contexts/SettingsContext.tsx"; +import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; + +// macOS daemon/CLI only accept utun (Darwin parses digits as the utun unit); Linux caps at IFNAMSIZ-1 = 15 chars. +const IS_MAC = System.IsMac(); +const INTERFACE_NAME_RE = IS_MAC ? /^utun\d+$/ : /^[A-Za-z0-9._-]{1,15}$/; +const INTERFACE_NAME_ERROR_KEY = IS_MAC + ? "settings.advanced.interfaceName.errorMac" + : "settings.advanced.interfaceName.error"; + +// Port 0 lets the daemon pick a random free port. +const PORT_MIN = 0; +const PORT_MAX = 65535; + +// Mirrors client/iface/iface.go MinMTU / MaxMTU. +const MTU_MIN = 576; +const MTU_MAX = 8192; + +const PSK_MASK = "**********"; + +export function SettingsAdvanced() { + const { t } = useTranslation(); + const { config, saveFields } = useSettings(); + const { mdm } = useRestrictions(); + + const initialPsk = config.preSharedKeySet ? PSK_MASK : ""; + + const [values, setValues] = useState({ + interfaceName: config.interfaceName, + wireguardPort: config.wireguardPort, + mtu: config.mtu, + }); + + const [pskInputValue, setPskInputValue] = useState(initialPsk); + const [saving, setSaving] = useState(false); + + useEffect(() => { + setValues({ + interfaceName: config.interfaceName, + wireguardPort: config.wireguardPort, + mtu: config.mtu, + }); + setPskInputValue(config.preSharedKeySet ? PSK_MASK : ""); + }, [config.interfaceName, config.wireguardPort, config.mtu, config.preSharedKeySet]); + + const errors = useMemo(() => { + const out: { interfaceName?: string; wireguardPort?: string; mtu?: string } = {}; + if (!INTERFACE_NAME_RE.test(values.interfaceName)) { + out.interfaceName = t(INTERFACE_NAME_ERROR_KEY); + } + if ( + !Number.isInteger(values.wireguardPort) || + values.wireguardPort < PORT_MIN || + values.wireguardPort > PORT_MAX + ) { + out.wireguardPort = t("settings.advanced.port.error", { + min: PORT_MIN, + max: PORT_MAX, + }); + } + if (!Number.isInteger(values.mtu) || values.mtu < MTU_MIN || values.mtu > MTU_MAX) { + out.mtu = t("settings.advanced.mtu.error", { min: MTU_MIN, max: MTU_MAX }); + } + return out; + }, [values.interfaceName, values.wireguardPort, values.mtu, t]); + + const filteredErrors = mdm.wireguardPort ? { ...errors, wireguardPort: undefined } : errors; + const hasErrors = Object.values(filteredErrors).some((v) => v !== undefined); + const pskChanged = pskInputValue !== initialPsk; + const hasChanges = + values.interfaceName !== config.interfaceName || + (!mdm.wireguardPort && values.wireguardPort !== config.wireguardPort) || + values.mtu !== config.mtu || + (!mdm.preSharedKey && pskChanged); + + const handleSave = async () => { + if (!hasChanges || saving || hasErrors) return; + setSaving(true); + try { + const partial: typeof values = { ...values }; + if (mdm.wireguardPort) partial.wireguardPort = config.wireguardPort; + + const pskEdited = !mdm.preSharedKey && pskChanged && pskInputValue !== PSK_MASK; + const pskOpts = pskEdited ? { preSharedKey: pskInputValue } : undefined; + await saveFields(partial, pskOpts); + if (pskEdited) setPskInputValue(pskInputValue === "" ? "" : PSK_MASK); + } finally { + setSaving(false); + } + }; + + return ( + <> + + setValues((v) => ({ ...v, interfaceName: e.target.value }))} + spellCheck={false} + autoComplete={"off"} + autoCorrect={"off"} + autoCapitalize={"off"} + /> +
    + {!mdm.wireguardPort && ( +
    + + setValues((v) => ({ + ...v, + wireguardPort: Number(e.target.value), + })) + } + /> + + {t("settings.advanced.port.help")} + +
    + )} + setValues((v) => ({ ...v, mtu: Number(e.target.value) }))} + /> +
    +
    + + {!mdm.preSharedKey && ( + +
    + + {t("settings.advanced.psk.help")} + setPskInputValue(e.target.value)} + spellCheck={false} + autoComplete={"new-password"} + autoCorrect={"off"} + autoCapitalize={"off"} + /> +
    +
    + )} + + + + + + ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsGeneral.tsx b/client/ui/frontend/src/modules/settings/SettingsGeneral.tsx new file mode 100644 index 000000000..05d40e15c --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsGeneral.tsx @@ -0,0 +1,113 @@ +import { useEffect, useId, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/buttons/Button"; +import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch"; +import { HelpText } from "@/components/typography/HelpText"; +import { Input } from "@/components/inputs/Input"; +import { Label } from "@/components/typography/Label"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useAutostartSetting, useSettings } from "@/contexts/SettingsContext.tsx"; +import { ManagementServerSwitch } from "@/components/ManagementServerSwitch.tsx"; +import { ManagementMode, useManagementUrl } from "@/hooks/useManagementUrl.ts"; +import { LanguagePicker } from "@/components/LanguagePicker.tsx"; +import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; + +export function SettingsGeneral() { + const { t } = useTranslation(); + const { config, setField } = useSettings(); + const { autostart, setAutostartEnabled } = useAutostartSetting(); + const { mode, setMode, setUrl, displayUrl, showError, canSave, save, checking, unreachable } = + useManagementUrl(); + const { mdm, features } = useRestrictions(); + + const inputRef = useRef(null); + const managementUrlId = useId(); + const prevMode = useRef(mode); + useEffect(() => { + if (prevMode.current === ManagementMode.Cloud && mode === ManagementMode.SelfHosted) { + inputRef.current?.focus(); + } + prevMode.current = mode; + }, [mode]); + + return ( + <> + + + setField("disableNotifications", !v)} + label={t("settings.general.notifications.label")} + helpText={t("settings.general.notifications.help")} + /> + {!mdm.disableAutoConnect && !features.disableUpdateSettings && ( + setField("disableAutoConnect", !v)} + label={t("settings.general.connectOnStartup.label")} + helpText={t("settings.general.connectOnStartup.help")} + /> + )} + {(autostart === null || autostart.supported) && ( + + )} + + + {!mdm.managementURL && !features.disableUpdateSettings && ( + +
    +
    +
    + + {t("settings.general.management.help")} +
    + +
    + {mode === ManagementMode.SelfHosted && ( +
    + setUrl(e.target.value)} + placeholder={t("settings.general.management.urlPlaceholder")} + error={ + showError + ? t("settings.general.management.urlError") + : undefined + } + warning={ + unreachable + ? t("settings.general.management.urlUnreachable") + : undefined + } + spellCheck={false} + autoComplete={"off"} + autoCorrect={"off"} + autoCapitalize={"off"} + /> + +
    + )} +
    +
    + )} + + ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx b/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx new file mode 100644 index 000000000..816dcb71f --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx @@ -0,0 +1,87 @@ +import { useTranslation } from "react-i18next"; +import { Tooltip } from "@/components/Tooltip.tsx"; +import { VerticalTabs } from "@/components/VerticalTabs.tsx"; +import { UpdateBadge } from "@/modules/auto-update/UpdateBadge.tsx"; +import { useClientVersion } from "@/contexts/ClientVersionContext.tsx"; +import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; +import { + BoltIcon, + InfoIcon, + LifeBuoyIcon, + NetworkIcon, + ShieldIcon, + SlidersHorizontalIcon, + SquareTerminalIcon, + UserCircleIcon, +} from "lucide-react"; + +export const SettingsNavigation = () => { + const { t } = useTranslation(); + const { updateAvailable } = useClientVersion(); + const { mdm, features } = useRestrictions(); + const showSsh = mdm.allowServerSSH ?? !features.disableUpdateSettings; + + const aboutAdornment = updateAvailable ? ( + + + + ) : undefined; + + return ( +
    + + + {!features.disableUpdateSettings && ( + <> + + + + )} + {!features.disableProfiles && ( + + )} + {showSsh && ( + + )} + {!features.disableUpdateSettings && ( + + )} + + + +
    + ); +}; diff --git a/client/ui/frontend/src/modules/settings/SettingsNetwork.tsx b/client/ui/frontend/src/modules/settings/SettingsNetwork.tsx new file mode 100644 index 000000000..32c6f8941 --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsNetwork.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from "react-i18next"; +import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useSettings } from "@/contexts/SettingsContext.tsx"; +import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; + +export function SettingsNetwork() { + const { t } = useTranslation(); + const { config, setField } = useSettings(); + const { mdm } = useRestrictions(); + + return ( + <> + + setField("lazyConnectionEnabled", v)} + label={t("settings.network.lazy.label")} + helpText={t("settings.network.lazy.help")} + /> + setField("networkMonitor", v)} + label={t("settings.network.monitor.label")} + helpText={t("settings.network.monitor.help")} + /> + + + + setField("disableDns", !v)} + label={t("settings.network.dns.label")} + helpText={t("settings.network.dns.help")} + /> + {!mdm.disableClientRoutes && ( + setField("disableClientRoutes", !v)} + label={t("settings.network.clientRoutes.label")} + helpText={t("settings.network.clientRoutes.help")} + /> + )} + {!mdm.disableServerRoutes && ( + setField("disableServerRoutes", !v)} + label={t("settings.network.serverRoutes.label")} + helpText={t("settings.network.serverRoutes.help")} + /> + )} + setField("disableIpv6", !v)} + label={t("settings.network.ipv6.label")} + helpText={t("settings.network.ipv6.help")} + /> + + + ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsPage.tsx b/client/ui/frontend/src/modules/settings/SettingsPage.tsx new file mode 100644 index 000000000..bf0db3bec --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsPage.tsx @@ -0,0 +1,131 @@ +import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { useLocation } from "react-router-dom"; +import { Events } from "@wailsio/runtime"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { cn } from "@/lib/cn"; +import { isMacOS } from "@/lib/platform"; +import { AppRightPanel } from "@/layouts/AppRightPanel.tsx"; +import { VerticalTabs } from "@/components/VerticalTabs.tsx"; +import { SettingsNavigation } from "@/modules/settings/SettingsNavigation.tsx"; +import { AutostartSettingsProvider, SettingsProvider } from "@/contexts/SettingsContext.tsx"; +import { SettingsGeneral } from "@/modules/settings/SettingsGeneral.tsx"; +import { SettingsNetwork } from "@/modules/settings/SettingsNetwork.tsx"; +import { SettingsSecurity } from "@/modules/settings/SettingsSecurity.tsx"; +import { ProfilesTab } from "@/modules/profiles/ProfilesTab.tsx"; +import { SettingsSSH } from "@/modules/settings/SettingsSSH.tsx"; +import { SettingsAdvanced } from "@/modules/settings/SettingsAdvanced.tsx"; +import { SettingsTroubleshooting } from "@/modules/settings/SettingsTroubleshooting.tsx"; +import { SettingsAbout } from "@/modules/settings/SettingsAbout.tsx"; +import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; + +const EVENT_SETTINGS_OPEN = "netbird:settings:open"; + +const enum Tab { + General = "general", + Network = "network", + Security = "security", + Profiles = "profiles", + SSH = "ssh", + Advanced = "advanced", + Troubleshooting = "troubleshooting", + About = "about", +} + +const TAB_CONTENT: Record = { + [Tab.General]: , + [Tab.Network]: , + [Tab.Security]: , + [Tab.Profiles]: , + [Tab.SSH]: , + [Tab.Advanced]: , + [Tab.Troubleshooting]: , + [Tab.About]: , +}; + +export const SettingsPage = () => { + const location = useLocation(); + const navState = location.state as { tab?: string } | null; + const { mdm, features } = useRestrictions(); + + const visibleTabs = useMemo(() => { + const editable = !features.disableUpdateSettings; + const visibility: Record = { + [Tab.General]: true, + [Tab.Network]: editable, + [Tab.Security]: editable, + [Tab.Profiles]: !features.disableProfiles, + [Tab.SSH]: mdm.allowServerSSH ?? editable, + [Tab.Advanced]: editable, + [Tab.Troubleshooting]: true, + [Tab.About]: true, + }; + return (Object.keys(visibility) as Tab[]).filter((t) => visibility[t]); + }, [features.disableUpdateSettings, features.disableProfiles, mdm.allowServerSSH]); + + const defaultTab = visibleTabs[0]; + const [active, setActive] = useState(() => navState?.tab ?? defaultTab); + + useEffect(() => { + if (navState?.tab) setActive(navState.tab); + }, [navState?.tab, location.key]); + + useEffect(() => { + return Events.On(EVENT_SETTINGS_OPEN, (e: { data: string }) => { + setActive(e.data || defaultTab); + }); + }, [defaultTab]); + + // Reset active tab if it got disabled by any feature flag or mdm restrictions + useEffect(() => { + if (!visibleTabs.includes(active as Tab)) setActive(defaultTab); + }, [visibleTabs, active, defaultTab]); + + return ( + <> + {isMacOS() ? ( +
    + ) : ( +
    + )} +
    + + + + + + + +
    + {visibleTabs.map((tab) => ( + + {TAB_CONTENT[tab]} + + ))} +
    +
    + + + +
    +
    +
    +
    +
    +
    + + ); +}; diff --git a/client/ui/frontend/src/modules/settings/SettingsSSH.tsx b/client/ui/frontend/src/modules/settings/SettingsSSH.tsx new file mode 100644 index 000000000..f18fd9493 --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsSSH.tsx @@ -0,0 +1,119 @@ +import { useTranslation } from "react-i18next"; +import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch"; +import { HelpText } from "@/components/typography/HelpText"; +import { Input } from "@/components/inputs/Input"; +import { Label } from "@/components/typography/Label"; +import { cn } from "@/lib/cn"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useSettings } from "@/contexts/SettingsContext.tsx"; +import { type ChangeEvent, useEffect, useId, useState } from "react"; + +export function SettingsSSH() { + const { t } = useTranslation(); + const { config, setField } = useSettings(); + const isSSHServerEnabled = config.serverSshAllowed; + const jwtTtlId = useId(); + const [jwtTtlInput, setJwtTtlInput] = useState(String(config.sshJwtCacheTtl)); + + useEffect(() => { + setJwtTtlInput(String(config.sshJwtCacheTtl)); + }, [config.sshJwtCacheTtl]); + + const handleJwtTtlChange = (e: ChangeEvent) => { + const v = e.target.value; + setJwtTtlInput(v); + if (v === "") return; + const n = Number(v); + if (Number.isFinite(n) && n >= 0) { + setField("sshJwtCacheTtl", n); + } + }; + + const handleJwtTtlBlur = () => { + if (jwtTtlInput === "") { + setJwtTtlInput("0"); + setField("sshJwtCacheTtl", 0); + return; + } + const n = Number(jwtTtlInput); + if (!Number.isFinite(n) || n < 0) { + setJwtTtlInput(String(config.sshJwtCacheTtl)); + } + }; + return ( + <> + + setField("serverSshAllowed", v)} + label={t("settings.ssh.server.label")} + helpText={t("settings.ssh.server.help")} + /> + + + + setField("enableSshRoot", v)} + label={t("settings.ssh.root.label")} + helpText={t("settings.ssh.root.help")} + /> + setField("enableSshSftp", v)} + label={t("settings.ssh.sftp.label")} + helpText={t("settings.ssh.sftp.help")} + /> + setField("enableSshLocalPortForwarding", v)} + label={t("settings.ssh.localForward.label")} + helpText={t("settings.ssh.localForward.help")} + /> + setField("enableSshRemotePortForwarding", v)} + label={t("settings.ssh.remoteForward.label")} + helpText={t("settings.ssh.remoteForward.help")} + /> + + + + setField("disableSshAuth", !v)} + label={t("settings.ssh.jwt.label")} + helpText={t("settings.ssh.jwt.help")} + /> +
    +
    + + {t("settings.ssh.jwtTtl.help")} +
    +
    + +
    +
    +
    + + ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsSection.tsx b/client/ui/frontend/src/modules/settings/SettingsSection.tsx new file mode 100644 index 000000000..adba65cdc --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsSection.tsx @@ -0,0 +1,43 @@ +import type { ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +export const SectionGroup = ({ + title, + children, + disabled = false, +}: { + title: string; + children: ReactNode; + disabled?: boolean; +}) => ( +
    +

    + {title} +

    +
    {children}
    +
    +); + +export const SettingsBottomBar = ({ children }: { children: ReactNode }) => ( + <> +
    +
    +
    + {children} +
    +
    + +); diff --git a/client/ui/frontend/src/modules/settings/SettingsSecurity.tsx b/client/ui/frontend/src/modules/settings/SettingsSecurity.tsx new file mode 100644 index 000000000..db1ad9c8f --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsSecurity.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from "react-i18next"; +import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useSettings } from "@/contexts/SettingsContext.tsx"; +import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; + +export function SettingsSecurity() { + const { t } = useTranslation(); + const { config, setField } = useSettings(); + const { mdm } = useRestrictions(); + const hideRosenpassEnabled = mdm.rosenpassEnabled; + const hideRosenpassPermissive = + mdm.rosenpassPermissive || (mdm.rosenpassEnabled && !config.rosenpassEnabled); + const showEncryptionSection = !(hideRosenpassEnabled && hideRosenpassPermissive); + + return ( + <> + + {!mdm.blockInbound && ( + setField("blockInbound", v)} + label={t("settings.security.blockInbound.label")} + helpText={t("settings.security.blockInbound.help")} + /> + )} + setField("blockLanAccess", v)} + label={t("settings.security.blockLan.label")} + helpText={t("settings.security.blockLan.help")} + /> + + + {showEncryptionSection && ( + + {!hideRosenpassEnabled && ( + { + setField("rosenpassEnabled", v); + if (!v) setField("rosenpassPermissive", false); + }} + label={t("settings.security.rosenpass.label")} + helpText={t("settings.security.rosenpass.help")} + /> + )} + {!hideRosenpassPermissive && ( + setField("rosenpassPermissive", v)} + label={t("settings.security.rosenpassPermissive.label")} + helpText={t("settings.security.rosenpassPermissive.help")} + disabled={!config.rosenpassEnabled} + /> + )} + + )} + + ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsSkeleton.tsx b/client/ui/frontend/src/modules/settings/SettingsSkeleton.tsx new file mode 100644 index 000000000..afce192e6 --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsSkeleton.tsx @@ -0,0 +1,27 @@ +import Skeleton from "react-loading-skeleton"; + +export const SettingsSkeleton = () => { + return ( +
    +
    + +
    + + +
    +
    + + +
    +
    +
    + +
    + + + +
    +
    +
    + ); +}; diff --git a/client/ui/frontend/src/modules/settings/SettingsTroubleshooting.tsx b/client/ui/frontend/src/modules/settings/SettingsTroubleshooting.tsx new file mode 100644 index 000000000..991937719 --- /dev/null +++ b/client/ui/frontend/src/modules/settings/SettingsTroubleshooting.tsx @@ -0,0 +1,337 @@ +import { useId, type ReactNode } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { CircleCheckBig, FolderOpen, Loader2 } from "lucide-react"; +import { Browser } from "@wailsio/runtime"; +import { Debug as DebugSvc } from "@bindings/services"; +import type { DebugBundleResult } from "@bindings/services/models.js"; +import { Button } from "@/components/buttons/Button"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch"; +import HelpText from "@/components/typography/HelpText.tsx"; +import { Input } from "@/components/inputs/Input"; +import { Label } from "@/components/typography/Label"; +import { SquareIcon } from "@/components/SquareIcon"; +import { formatRemaining } from "@/lib/formatters"; +import type { DebugStage } from "@/contexts/DebugBundleContext"; +import { useDebugBundleContext } from "@/contexts/DebugBundleContext"; +import { SectionGroup, SettingsBottomBar } from "@/modules/settings/SettingsSection.tsx"; + +const SUPPORT_DOCS_URL = "https://docs.netbird.io/help/report-bug-issues"; + +export function SettingsTroubleshooting() { + const { t } = useTranslation(); + const durationId = useId(); + const { + anonymize, + setAnonymize, + systemInfo, + setSystemInfo, + upload, + setUpload, + trace, + setTrace, + capture, + setCapture, + traceMinutes, + setTraceMinutes, + capturePackets, + setCapturePackets, + run, + stage, + cancel, + reset, + } = useDebugBundleContext(); + + if (stage.kind === "done") { + return ( + + ); + } + if (stage.kind !== "idle") { + return ; + } + + return ( + + + + + + +
    + +
    +
    + + + {t("settings.troubleshooting.duration.help")} + +
    +
    + + setTraceMinutes( + Math.max(1, Math.min(30, Number(e.target.value) || 1)), + ) + } + customSuffix={t("settings.troubleshooting.duration.suffix")} + disabled={!capture} + /> +
    +
    +
    + + + + +
    + ); +} + +function CenteredPanel({ children }: Readonly<{ children: ReactNode }>) { + return ( +
    + {children} +
    + ); +} + +function ProgressSection({ + stage, + onCancel, +}: Readonly<{ stage: DebugStage; onCancel: () => void }>) { + const { t } = useTranslation(); + const cancelling = stage.kind === "cancelling"; + return ( + + + +
    + {stageLabel(stage, t)} + + {t("settings.troubleshooting.progress.description")} + +
    + + {stage.kind === "capturing" && ( +
    + {formatRemaining(stage.remainingSec)} +
    + )} + + + + +
    + ); +} + +function DoneResult({ + result, + uploaded, + onClose, +}: Readonly<{ + result: DebugBundleResult; + uploaded: boolean; + onClose: () => void; +}>) { + const { t } = useTranslation(); + const showKey = uploaded && Boolean(result.uploadedKey); + const uploadFailed = uploaded && !result.uploadedKey; + const onRevealPath = () => { + if (!result.path) return; + DebugSvc.RevealFile(result.path).catch((err: unknown) => + console.error("reveal debug bundle file", err), + ); + }; + return ( + + + +
    + + {showKey + ? t("settings.troubleshooting.done.uploadedTitle") + : t("settings.troubleshooting.done.savedTitle")} + + + {showKey ? ( + { + e.preventDefault(); + Browser.OpenURL(SUPPORT_DOCS_URL).catch(() => + globalThis.open(SUPPORT_DOCS_URL, "_blank"), + ); + }} + className={"text-netbird hover:underline"} + > + {/* content is provided by */} + + + ), + }} + /> + ) : ( + t("settings.troubleshooting.done.savedDescription") + )} + +
    + +
    + {showKey && } + + {result.path && !showKey && ( + + + + } + /> + )} + + {uploadFailed && ( +
    + {result.uploadFailureReason + ? t("settings.troubleshooting.uploadFailedWithReason", { + reason: result.uploadFailureReason, + }) + : t("settings.troubleshooting.uploadFailed")} +
    + )} +
    + + + {showKey ? ( + + ) : ( + result.path && ( + + ) + )} + + +
    + ); +} + +const stageLabel = ( + stage: DebugStage, + t: (key: string, options?: Record) => string, +): string => { + switch (stage.kind) { + case "reconnecting": + return t("settings.troubleshooting.stage.reconnecting"); + case "capturing": + return t("settings.troubleshooting.stage.capturing"); + case "bundling": + return t("settings.troubleshooting.stage.bundling"); + case "uploading": + return t("settings.troubleshooting.stage.uploading"); + case "cancelling": + return t("settings.troubleshooting.stage.cancelling"); + default: + return ""; + } +}; diff --git a/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx b/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx new file mode 100644 index 000000000..7be59f102 --- /dev/null +++ b/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx @@ -0,0 +1,170 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + Preferences, + Profiles as ProfilesSvc, + Settings as SettingsSvc, + WindowManager, +} from "@bindings/services"; +import { Restrictions, SetConfigParams } from "@bindings/services/models.js"; +import { ConfirmDialog } from "@/components/dialog/ConfirmDialog"; +import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow"; +import { errorDialog, formatErrorMessage } from "@/lib/errors"; +import i18next from "@/lib/i18n"; +import { isNetbirdCloud } from "@/hooks/useManagementUrl"; +import { WelcomeStepTray } from "./WelcomeStepTray"; +import { WelcomeStepManagement } from "./WelcomeStepManagement"; + +const WINDOW_WIDTH = 360; + +type WelcomeStep = "tray" | "management"; + +function shouldShowManagementStep( + activeProfileId: string, + email: string, + managementUrl: string, + managedManagementUrl: string, +): boolean { + if (managedManagementUrl) return false; + // The default profile's ID equals the literal "default", so this check + // holds whether we pass an ID or the legacy name. + if (activeProfileId !== "default") return false; + if (email.trim() !== "") return false; + return isNetbirdCloud(managementUrl); +} + +type InitialState = { + profileName: string; + username: string; + managementUrl: string; + needsManagementStep: boolean; +}; + +export default function WelcomeDialog() { + const [step, setStep] = useState("tray"); + const [initial, setInitial] = useState(null); + const [closing, setClosing] = useState(false); + const contentRef = useAutoSizeWindow(WINDOW_WIDTH, initial !== null); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const [username, active] = await Promise.all([ + ProfilesSvc.Username(), + ProfilesSvc.GetActive(), + ]); + const profileId = active.id || "default"; + const [config, list, restrictions] = await Promise.all([ + SettingsSvc.GetConfig({ profileName: profileId, username }), + ProfilesSvc.List(username), + SettingsSvc.GetRestrictions().catch(() => new Restrictions()), + ]); + const profile = list.find((p) => p.id === profileId); + const email = profile?.email ?? ""; + if (cancelled) return; + setInitial({ + profileName: profileId, + username, + managementUrl: config.managementUrl, + needsManagementStep: shouldShowManagementStep( + profileId, + email, + config.managementUrl, + restrictions.mdm.managementURL, + ), + }); + } catch (e) { + console.error("welcome: initial probe failed", e); + if (cancelled) return; + setInitial({ + profileName: "default", + username: "", + managementUrl: "", + needsManagementStep: false, + }); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const finish = useCallback(async () => { + if (closing) return; + setClosing(true); + try { + await Preferences.SetOnboardingCompleted(true); + } catch (e) { + console.error("persist onboarding flag:", e); + } + try { + await WindowManager.OpenMain(); + } catch (e) { + console.error("open main window:", e); + } + try { + await WindowManager.CloseWelcome(); + } catch (e) { + console.error("close welcome window:", e); + } + }, [closing]); + + const handleTrayContinue = useCallback(async () => { + if (initial?.needsManagementStep) { + setStep("management"); + } else { + await finish(); + } + }, [initial, finish]); + + const handleManagementContinue = useCallback( + async (url: string) => { + if (!initial) return; + try { + // SetConfig is a partial update — undefined fields are preserved Go-side. + await SettingsSvc.SetConfig( + new SetConfigParams({ + profileName: initial.profileName, + username: initial.username, + managementUrl: url, + }), + ); + } catch (e) { + await errorDialog({ + Title: i18next.t("settings.error.saveTitle"), + Message: formatErrorMessage(e), + }); + throw e; + } + setInitial((s) => (s ? { ...s, managementUrl: url } : s)); + await finish(); + }, + [initial, finish], + ); + + const content = useMemo(() => { + if (!initial) { + return null; + } + switch (step) { + case "tray": + return ; + case "management": + return ( + + ); + } + }, [initial, step, handleTrayContinue, handleManagementContinue]); + + return ( + + {content} + + ); +} diff --git a/client/ui/frontend/src/modules/welcome/WelcomeStepManagement.tsx b/client/ui/frontend/src/modules/welcome/WelcomeStepManagement.tsx new file mode 100644 index 000000000..5cb04d484 --- /dev/null +++ b/client/ui/frontend/src/modules/welcome/WelcomeStepManagement.tsx @@ -0,0 +1,135 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/buttons/Button"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import { Input } from "@/components/inputs/Input"; +import { ManagementServerSwitch } from "@/components/ManagementServerSwitch"; +import { + CLOUD_MANAGEMENT_URL, + ManagementMode, + checkManagementUrlReachable, + isNetbirdCloud, + isValidManagementUrl, + normalizeManagementUrl, +} from "@/hooks/useManagementUrl"; +import { cn } from "@/lib/cn.ts"; +import { isMacOS } from "@/lib/platform.ts"; + +type WelcomeStepManagementProps = { + initialUrl: string; + onContinue: (url: string) => Promise; +}; + +export function WelcomeStepManagement({ + initialUrl, + onContinue, +}: Readonly) { + const { t } = useTranslation(); + const startsCloud = isNetbirdCloud(initialUrl); + const [mode, setMode] = useState( + startsCloud ? ManagementMode.Cloud : ManagementMode.SelfHosted, + ); + const [url, setUrl] = useState(startsCloud ? "" : initialUrl); + const [syntaxError, setSyntaxError] = useState(null); + const [unreachable, setUnreachable] = useState(false); + const [checking, setChecking] = useState(false); + + const trimmedUrl = url.trim(); + const syntaxValid = mode === ManagementMode.Cloud || isValidManagementUrl(trimmedUrl); + const inputRef = useRef(null); + const initialMountRef = useRef(true); + const initialSelfHostedRef = useRef(!startsCloud); + + useEffect(() => { + setSyntaxError(null); + setUnreachable(false); + }, [url, mode]); + + useEffect(() => { + if (initialMountRef.current && initialSelfHostedRef.current) { + inputRef.current?.focus(); + } + initialMountRef.current = false; + }, []); + + const handleContinue = useCallback(async () => { + if (checking) return; + if (mode === ManagementMode.SelfHosted && (!trimmedUrl || !syntaxValid)) { + setSyntaxError(t("welcome.management.urlInvalid")); + inputRef.current?.focus(); + return; + } + const target = + mode === ManagementMode.Cloud + ? CLOUD_MANAGEMENT_URL + : normalizeManagementUrl(trimmedUrl); + if (mode === ManagementMode.SelfHosted && !unreachable) { + setChecking(true); + const reachable = await checkManagementUrlReachable(target); + setChecking(false); + if (!reachable) { + setUnreachable(true); + return; + } + } + try { + await onContinue(target); + } catch (e) { + console.error("save management url:", e); + } + }, [checking, mode, syntaxValid, trimmedUrl, unreachable, onContinue, t]); + + const inputError = syntaxError ?? undefined; + const inputWarning = useMemo( + () => (!syntaxError && unreachable ? t("welcome.management.urlUnreachable") : undefined), + [syntaxError, unreachable, t], + ); + + return ( + <> +
    + + {t("welcome.management.title")} + + + {t("welcome.management.description")} + +
    + +
    + +
    + + {mode === ManagementMode.SelfHosted && ( +
    + setUrl(e.target.value)} + error={inputError} + warning={inputWarning} + spellCheck={false} + autoComplete={"off"} + autoCorrect={"off"} + autoCapitalize={"off"} + /> +
    + )} + + + + + + ); +} diff --git a/client/ui/frontend/src/modules/welcome/WelcomeStepTray.tsx b/client/ui/frontend/src/modules/welcome/WelcomeStepTray.tsx new file mode 100644 index 000000000..fe06abc20 --- /dev/null +++ b/client/ui/frontend/src/modules/welcome/WelcomeStepTray.tsx @@ -0,0 +1,58 @@ +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/buttons/Button"; +import { DialogActions } from "@/components/dialog/DialogActions"; +import { DialogDescription } from "@/components/dialog/DialogDescription"; +import { DialogHeading } from "@/components/dialog/DialogHeading"; +import { isMacOS, isWindows } from "@/lib/platform"; +import trayScreenshotDarwin from "@/assets/img/tray-darwin.png"; +import trayScreenshotWindows from "@/assets/img/tray-windows.png"; +import trayScreenshotLinux from "@/assets/img/tray-linux.png"; + +// Call at render time, not module scope: initPlatform() must run before isMacOS/isWindows. +function trayScreenshotForOS(): string { + if (isMacOS()) return trayScreenshotDarwin; + if (isWindows()) return trayScreenshotWindows; + return trayScreenshotLinux; +} + +type WelcomeStepTrayProps = { + onContinue: () => void; +}; + +export function WelcomeStepTray({ onContinue }: Readonly) { + const { t } = useTranslation(); + const trayScreenshot = trayScreenshotForOS(); + + return ( + <> +
    + {""} +
    + +
    + + {t("welcome.title")} + + {t("welcome.description")} +
    + + + + + + ); +} diff --git a/client/ui/frontend/src/vite-env.d.ts b/client/ui/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/client/ui/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/client/ui/frontend/tailwind.config.ts b/client/ui/frontend/tailwind.config.ts new file mode 100644 index 000000000..93ff39eea --- /dev/null +++ b/client/ui/frontend/tailwind.config.ts @@ -0,0 +1,177 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + darkMode: "class", + theme: { + fontFamily: { + sans: ['"Inter Variable"', 'ui-sans-serif', 'system-ui', 'sans-serif'], + mono: ['"JetBrains Mono Variable"', 'ui-monospace', 'monospace'], + }, + extend: { + colors: { + "nb-gray": { + DEFAULT: "#181A1D", + 50: "#f4f6f7", + 100: "#e4e7e9", + 200: "#cbd2d6", + 250: "#b7c0c6", + 300: "#a3adb5", + 350: "#8f9ca8", + 400: "#7c8994", + 500: "#616e79", + 600: "#535d67", + 700: "#474e57", + 800: "#3f444b", + 850: "#363b40", + 900: "#2e3238", + 910: "#2b2f33", + 920: "#25282d", + 925: "#1e2123", + 930: "#25282c", + 935: "#1f2124", + 940: "#1c1e21", + 950: "#181a1d", + 960: "#16181b", + }, + gray: { + 50: "#F9FAFB", + 100: "#F3F4F6", + 200: "#E5E7EB", + 300: "#D1D5DB", + 400: "#9CA3AF", + 500: "#6B7280", + 600: "#4B5563", + 700: "#374151", + 800: "#1F2937", + 900: "#111827", + }, + red: { + 50: "#FDF2F2", + 100: "#FDE8E8", + 200: "#FBD5D5", + 300: "#F8B4B4", + 400: "#F98080", + 500: "#F05252", + 600: "#E02424", + 700: "#C81E1E", + 800: "#9B1C1C", + 900: "#771D1D", + }, + yellow: { + 50: "#FDFDEA", + 100: "#FDF6B2", + 200: "#FCE96A", + 300: "#FACA15", + 400: "#E3A008", + 500: "#C27803", + 600: "#9F580A", + 700: "#8E4B10", + 800: "#723B13", + 900: "#633112", + }, + green: { + 50: "#F3FAF7", + 100: "#DEF7EC", + 200: "#BCF0DA", + 300: "#84E1BC", + 400: "#31C48D", + 500: "#0E9F6E", + 600: "#057A55", + 700: "#046C4E", + 800: "#03543F", + 900: "#014737", + }, + blue: { + 50: "#EBF5FF", + 100: "#E1EFFE", + 200: "#C3DDFD", + 300: "#A4CAFE", + 400: "#76A9FA", + 500: "#3F83F8", + 600: "#1C64F2", + 700: "#1A56DB", + 800: "#1E429F", + 900: "#233876", + }, + indigo: { + 50: "#F0F5FF", + 100: "#E5EDFF", + 200: "#CDDBFE", + 300: "#B4C6FC", + 400: "#8DA2FB", + 500: "#6875F5", + 600: "#5850EC", + 700: "#5145CD", + 800: "#42389D", + 900: "#362F78", + }, + purple: { + 50: "#F6F5FF", + 100: "#EDEBFE", + 200: "#DCD7FE", + 300: "#CABFFD", + 400: "#AC94FA", + 500: "#9061F9", + 600: "#7E3AF2", + 700: "#6C2BD9", + 800: "#5521B5", + 900: "#4A1D96", + }, + pink: { + 50: "#FDF2F8", + 100: "#FCE8F3", + 200: "#FAD1E8", + 300: "#F8B4D9", + 400: "#F17EB8", + 500: "#E74694", + 600: "#D61F69", + 700: "#BF125D", + 800: "#99154B", + 900: "#751A3D", + }, + netbird: { + DEFAULT: "#f68330", + 50: "#fff6ed", + 100: "#feecd6", + 150: "#ffdfb8", + 200: "#ffd4a6", + 300: "#fab677", + 400: "#f68330", + 500: "#f46d1b", + 600: "#e55311", + 700: "#be3e10", + 800: "#973215", + 900: "#7a2b14", + 950: "#421308", + }, + }, + backgroundImage: { + "conic-netbird": "conic-gradient(from 0deg, #e55311 0%, #f68330 10%, #e55311 20%, #e55311 100%)", + }, + keyframes: { + "pulse-reverse": { + "0%, 100%": { opacity: "1" }, + "50%": { opacity: "0.4" }, + }, + "spin-slow": { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, + "ping-slow": { + "0%": { transform: "scale(1)", opacity: "1" }, + "75%, 100%": { transform: "scale(2)", opacity: "0" }, + }, + }, + animation: { + "ping-slow": "ping-slow 2s cubic-bezier(0, 0, 0.2, 1) infinite", + "pulse-slow": "pulse-reverse 2s cubic-bezier(0.5, 0, 0.6, 1) infinite", + "pulse-slower": "pulse-reverse 3s cubic-bezier(0.5, 0, 0.6, 1) infinite", + "spin-slow": "spin-slow 2s linear infinite", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; + +export default config; diff --git a/client/ui/frontend/tsconfig.json b/client/ui/frontend/tsconfig.json new file mode 100644 index 000000000..f95ce9015 --- /dev/null +++ b/client/ui/frontend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": false, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@bindings/*": ["bindings/github.com/netbirdio/netbird/client/ui/*"] + } + }, + "include": ["src", "bindings"], +} diff --git a/client/ui/frontend/vite.config.ts b/client/ui/frontend/vite.config.ts new file mode 100644 index 000000000..48b1c5630 --- /dev/null +++ b/client/ui/frontend/vite.config.ts @@ -0,0 +1,28 @@ +import path from "path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import wails from "@wailsio/runtime/plugins/vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + "@bindings": path.resolve( + __dirname, + "./bindings/github.com/netbirdio/netbird/client/ui", + ), + }, + }, + plugins: [react(), wails("./bindings")], + server: { + host: "127.0.0.1", + port: 9245, + strictPort: true, + fs: { + // The i18n bundles live at ../i18n/locales (shared with the Go tray). + // Whitelist the parent dir so Vite's dev server serves them. + allow: [path.resolve(__dirname, ".."), __dirname], + }, + }, +}); diff --git a/client/ui/grpc.go b/client/ui/grpc.go new file mode 100644 index 000000000..c8e3aed76 --- /dev/null +++ b/client/ui/grpc.go @@ -0,0 +1,67 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "fmt" + "runtime" + "strings" + "sync" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials/insecure" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/client/ui/desktop" +) + +// Conn is the lazy, lock-protected gRPC connection shared by all services so they reuse one channel. +type Conn struct { + addr string + + mu sync.Mutex + client proto.DaemonServiceClient +} + +func NewConn(addr string) *Conn { + return &Conn{addr: addr} +} + +func (c *Conn) Client() (proto.DaemonServiceClient, error) { + c.mu.Lock() + defer c.mu.Unlock() + if c.client != nil { + return c.client, nil + } + + cc, err := grpc.NewClient( + strings.TrimPrefix(c.addr, "tcp://"), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUserAgent(desktop.GetUIUserAgent()), + // Cap reconnect backoff at 5s; gRPC's default 120s MaxDelay would + // leave the UI waiting 30-60s to notice a freshly-started daemon. + grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: backoff.Config{ + BaseDelay: 1 * time.Second, + Multiplier: 1.6, + Jitter: 0.2, + MaxDelay: 5 * time.Second, + }, + }), + ) + if err != nil { + return nil, fmt.Errorf("dial daemon: %w", err) + } + c.client = proto.NewDaemonServiceClient(cc) + return c.client, nil +} + +// DaemonAddr returns the default daemon gRPC address: a Unix socket on Linux/macOS, TCP loopback on Windows. +func DaemonAddr() string { + if runtime.GOOS == "windows" { + return "tcp://127.0.0.1:41731" + } + return "unix:///var/run/netbird.sock" +} diff --git a/client/ui/guilog/debuglog.go b/client/ui/guilog/debuglog.go new file mode 100644 index 000000000..3a25c26c8 --- /dev/null +++ b/client/ui/guilog/debuglog.go @@ -0,0 +1,77 @@ +//go:build !android && !ios && !freebsd && !js + +// Package guilog manages gui-client.log, which follows the daemon's log level: +// in debug/trace the GUI attaches a rotated file alongside the console so its +// (and the React frontend's forwarded) output is captured for the debug bundle. +package guilog + +import ( + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/util" +) + +// DebugLog attaches/detaches gui-client.log based on the daemon's log level, +// fed via Apply. The file is left on disk for the debug bundle to collect. +// Disabled (and never touches logging) when the user set --log-file explicitly. +type DebugLog struct { + uiPath string + enabled bool + + mu sync.Mutex + fileOn bool +} + +// NewDebugLog builds the GUI debug log. enabled is false when the user passed +// --log-file (manual override). +func NewDebugLog(uiPath string, enabled bool) *DebugLog { + return &DebugLog{uiPath: uiPath, enabled: enabled} +} + +// Path returns the GUI log path to register with the daemon, or "" when disabled +// so the daemon won't collect a file the GUI never writes. +func (d *DebugLog) Path() string { + if !d.enabled { + return "" + } + return d.uiPath +} + +// Apply reacts to a daemon log level (the logrus name, e.g. "debug"). +// Idempotent via the fileOn guard, so the startup replay plus a racing +// change-event are harmless. +func (d *DebugLog) Apply(level string) { + if !d.enabled { + return + } + + // Compared numerically so there are no hard-coded level-name literals. + lvl, err := log.ParseLevel(level) + if err != nil { + lvl = log.InfoLevel + } + debug := lvl >= log.DebugLevel + + d.mu.Lock() + defer d.mu.Unlock() + + switch { + case debug && !d.fileOn: + if err := util.SetLogOutputs(log.StandardLogger(), util.LogConsole, d.uiPath); err != nil { + log.Errorf("attach GUI file log %s: %v", d.uiPath, err) + return + } + log.SetLevel(lvl) + d.fileOn = true + log.Infof("GUI file logging enabled (daemon level %s), writing to %s", level, d.uiPath) + case !debug && d.fileOn: + if err := util.SetLogOutputs(log.StandardLogger(), util.LogConsole); err != nil { + log.Errorf("detach GUI file log: %v", err) + } + log.SetLevel(log.InfoLevel) + d.fileOn = false + log.Infof("GUI file logging disabled (daemon level: %s)", level) + } +} diff --git a/client/ui/i18n/TRANSLATING.md b/client/ui/i18n/TRANSLATING.md new file mode 100644 index 000000000..88cbb8b11 --- /dev/null +++ b/client/ui/i18n/TRANSLATING.md @@ -0,0 +1,133 @@ +# Translating the NetBird UI + +A short brief for translating the desktop UI — for any translator, human or AI agent (*"you"* = whoever's translating). + +**Drive an agent with:** *"Read `i18n/TRANSLATING.md` and translate the UI to Russian"* — or *"…and review the existing German translation."* + +> 💡 **The one habit that matters most:** read each key's `description` before translating it. Labels are terse and ambiguous on their own; the `description` tells you what the string is, where it shows up, what to keep verbatim, and what it actually means. + +--- + +## What NetBird is + +A **business zero-trust VPN** — an encrypted **overlay mesh** between a company's devices, built on **WireGuard®**, connecting peers directly with a **relay** fallback. This is the **desktop client** (tray app + windows) someone runs to connect, switch profiles, browse peers, and pick an exit node — *not* the admin dashboard. + +**Audience:** IT-literate professionals. **Tone:** clear and professional, never consumer-cute. + +**The vocabulary you'll meet:** + +| Term | What it means here | +|---|---| +| **Peer** | A device on the network (laptop, server, phone) | +| **Resource / Network** | A routed network or service reachable through NetBird (UI calls these "Resources") | +| **Exit Node** | A peer that routes *all* internet traffic, like a full-tunnel gateway | +| **Profile** | A saved connection identity you can switch between | +| **Daemon** | The background service the UI talks to | +| **Management server** | The control plane — *Cloud* (hosted) or *self-hosted* (customer-run) | +| **Relay** | Forwards traffic when two peers can't connect directly | +| **Rosenpass** | Post-quantum security layered over WireGuard® | +| **Handshake** | The periodic WireGuard® key sync between peers | + +--- + +## The files + +``` +i18n/locales/_index.json shipped-language list +i18n/locales/en/common.json source of truth — message + description +i18n/locales//common.json a target — message only +``` + +Chrome-extension JSON, each key → `{ "message", "description" }`. You translate the **`message`**. + +| ✅ Do | ❌ Don't | +|---|---| +| Keep **every key** from `en`, in the same order | Translate, rename, reorder, drop, or add keys (they're identifiers; the set grows over time) | +| Put **only `message`** in target bundles | Copy `description` into a target bundle | +| Give every key a non-empty `message` | Leave keys missing or empty | +| Save valid UTF-8 JSON, no BOM | Add trailing commas or break the JSON | + +--- + +## Hard rules — get these exactly right + +These are the usual ways a translation *breaks the app*, not just reads oddly. + +| ✅ Do | ❌ Don't | +|---|---| +| Copy `{placeholders}` verbatim — `{version}`, `{count}`, `{name}`… | Translate the word inside the braces (`{verbleibend}` breaks it) | +| Reposition a placeholder so the sentence flows | Drop or duplicate a placeholder | +| Preserve every `\n`, leading/trailing space, and trailing `...` | Trim "invisible" spaces or the `...` (they're load-bearing) | +| Keep `®` in WireGuard® and quotes around `{name}` | Strip punctuation the description flags | + +**Plurals:** the app has only a *one / other* split — the singular key fires only when `count == 1`; the `{count}` key covers everything else (0, 2, 5, 100…). Languages with more than two forms (ru, pl, uk) can't be fully correct here — use the form that fits the widest range (Russian genitive plural: `минут` / `часов` / `дней`). Don't invent extra keys or cram multiple forms into one string. When no single form fits every value — a unit label after a number field, say — reach for a number-agnostic form (an abbreviation, or wording that reads the same for 1 and 100) instead of forcing a plural the *one / other* split can't supply. + +**Agreement:** a `{placeholder}` drops a value into a fixed frame, so the words around it must fit *every* value the app can supply. In inflected languages, write the frame in the case the surrounding preposition demands — German's duration fragments are **dative** because they land inside "…in {remaining}" (`in {count} Tagen`, `weniger als einer Minute`), not nominative `Tage`. Check the key that *consumes* the fragment (here `tray.session.expiresIn`) before choosing the form. + +--- + +## Glossary + +**Tier A — never translate (brands):** `NetBird` · `WireGuard®` · `Rosenpass` · `GitHub` · `ICE` · company/product names · sample URLs · version numbers. + +When a brand sits beside a common noun, keep its exact spelling but join them the way your language builds such phrases — a hyphen, a connector word, an inflected noun — rather than copying English's bare noun-stack. + +**Tier B — keep as-is (acronyms):** `SSO` · `MFA` · `DNS` · `IP`/`IPv6` · `ACL` · `SSH` · `GUI` · `P2P` · `URL` · `TCP`/`UDP`. + +**Tier C — judgment.** One rule decides every term: + +> **Use the word that language's IT users actually say.** Translate when a natural, common term exists; keep the English term *only* when the literal translation would be awkward or no one in that field really uses it. + +Apply each term **consistently** — same English term → same translation everywhere — and keep a term once you've settled it. Whether a term stays English or takes a native word is **language-dependent**: a technical loanword (e.g. *Daemon*, *Handshake*) often stays, an everyday word (e.g. *Latency*, *Public key*) usually localizes, and some (*Exit Node*, *Peer*) go either way depending on the language. Decide per term with the rule above — a foreign origin alone is no reason to keep English. **Your main reference is the existing bundles:** match how a term was already rendered for your language rather than re-deciding it. + +Two checks before you commit a term: + +- **Prefer established localized wording.** If a widely used tool in this space (for example WireGuard) ships your language, its wording for a shared term such as *handshake* is what users already expect — look at the translated app, not just English docs. For generic UI verbs and formal address, follow your OS vendor's style guide (Microsoft / Apple / Google). +- **Watch for false friends.** A literal translation can collide with a *different* established term in your field — confirm your word doesn't already mean something else in this domain before using it. + +--- + +## Style + +| ✅ Do | ❌ Don't | +|---|---| +| Use the **formal "you"** (de *Sie*, fr *vous*, ru *вы*, it *Lei*, zh 您) | Use casual/informal address | +| Keep **buttons, menu, and tray** items short, in your language's action form (de "Speichern", fr "Enregistrer") | Let a label run much longer than the English — space is tight | +| Follow **locale punctuation** (fr NBSP + « », de „…", zh full-width), including around a quoted UI label | Carry over English Title Case (use sentence case; German nouns excepted) | +| Translate a term the **same way everywhere** | Vary wording for the same concept across screens | + +Where it reads naturally, aim to keep each string **roughly the same length** as the English — the UI is tight and over-long strings can wrap or truncate. It's a soft preference, not a rule: if your language simply needs more words, use them. + +A few habits that keep a bundle reading like one product rather than a word-for-word port: + +- **Translate meaning, not words.** Render what a string *does*. An idiom or an awkward source phrase should become natural in your language, not a literal calque. +- **Keep one voice within a family.** Sibling strings — the connection states, every settings *help* caption, every "… Failed" title — should share a grammatical form. If one member sounds wrong in that form, re-voice the whole family rather than leave one odd sibling. +- **Mirror opposites.** A status should read as the natural counterpart of its pair: translate *Disconnected* as the opposite of however you rendered *Connected*, not as an unrelated word. Same for Active/Inactive, Selected/Not selected. +- **Give a standalone label its subject.** A bare button or title can lose the context the surrounding English UI implied — add the noun back if it would otherwise read ambiguously. + +--- + +## Procedure + +**New language** — read `en/common.json` *with* descriptions → settle your Tier C terms → write `i18n/locales//common.json` (same keys and order as `en`, `message` only, placeholders & brands preserved) → add a row to `_index.json` (`{"code","displayName"` = native name`,"englishName"}`) → run the QA list. Use the locale-code style the existing entries use (e.g. `fr`, `pt`, `zh-CN`). + +**Review (de / hu / …)** — read source and target side by side; for each key check glossary conformance (e.g. de `Exit-Node` → `Exit Node`, hu `Kilépő csomópont` → `Exit Node`), placeholder/`\n` integrity, consistency, tone, and that the meaning matches the English `description`. Fix in place, then report what you changed (especially term standardizations) so a native speaker can sanity-check. + +--- + +## QA before you finish + +- [ ] Valid JSON · **every `en` key** present, same order · **no `description`** fields +- [ ] Every `{placeholder}`, `\n`, and intentional space preserved · `...` / `… Failed` / `{name}` quotes kept +- [ ] Tier A/B left intact · Tier C applied consistently (and matching the existing bundle for your language) +- [ ] Buttons & tray short · locale punctuation and capitalization applied +- [ ] New language added to `_index.json` +- [ ] **Tested in the running app** ↓ + +--- + +## Test it in the app + +A bundle can pass every check above and still read wrong on screen. **Run the app, switch to your language, and click through the real surfaces** — tray menu, main window, every Settings tab, the dialogs. Watch for text overflow or truncation, labels that are technically right but wrong *for what the control does*, leaked placeholders, and terms that drift between screens. + +How to run the app and switch language: see the project README. Can't run it (e.g. a headless agent)? Say so in your summary — don't silently skip this step. diff --git a/client/ui/i18n/bundle.go b/client/ui/i18n/bundle.go new file mode 100644 index 000000000..892916999 --- /dev/null +++ b/client/ui/i18n/bundle.go @@ -0,0 +1,196 @@ +//go:build !android && !ios && !freebsd && !js + +// Package i18n loads and serves translation strings for both the tray (Go) +// and the React UI (via the services.I18n facade). +// +// The locale tree is passed in as an fs.FS so the embed directive can live in +// the main binary. +package i18n + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "path" + "sort" + "strings" + "sync" + + log "github.com/sirupsen/logrus" +) + +const ( + localeIndexFile = "_index.json" + + // commonBundleFile shape is Chrome-extension JSON (key -> "message" plus + // optional Crowdin "description"); loadBundle flattens to key->message. + commonBundleFile = "common.json" +) + +// LanguageCode is a BCP-47-ish locale identifier ("en", "hu", ...). +type LanguageCode string + +// DefaultLanguage is the fallback bundle for missing keys and the default +// when no preference is on disk. +const DefaultLanguage LanguageCode = "en" + +var ErrUnsupportedLanguage = errors.New("unsupported language") + +// Language describes one shipped UI locale. DisplayName is in the locale's +// own script (a Hungarian entry reads "Magyar" regardless of UI language). +type Language struct { + Code LanguageCode `json:"code"` + DisplayName string `json:"displayName"` + EnglishName string `json:"englishName"` +} + +type localeIndex struct { + Languages []Language `json:"languages"` +} + +// Bundle holds the parsed translation bundles. Loaded once at construction +// and never mutated. +type Bundle struct { + mu sync.RWMutex + languages []Language + bundles map[LanguageCode]map[string]string +} + +// NewBundle parses _index.json plus every /common.json in the locale +// tree. Hard-fails only when the default language is missing; other locales +// without a bundle are dropped with a warning. +func NewBundle(localesFS fs.FS) (*Bundle, error) { + idx, err := loadLocaleIndex(localesFS) + if err != nil { + return nil, fmt.Errorf("load locale index: %w", err) + } + + bundles := make(map[LanguageCode]map[string]string, len(idx.Languages)) + available := make([]Language, 0, len(idx.Languages)) + for _, l := range idx.Languages { + b, err := loadBundle(localesFS, l.Code) + if err != nil { + log.Warnf("skip language %q: %v", l.Code, err) + continue + } + bundles[l.Code] = b + available = append(available, l) + } + + if _, ok := bundles[DefaultLanguage]; !ok { + return nil, fmt.Errorf("default language %q bundle missing", DefaultLanguage) + } + + sort.Slice(available, func(i, j int) bool { return available[i].Code < available[j].Code }) + + return &Bundle{ + languages: available, + bundles: bundles, + }, nil +} + +// Languages returns a copy of the available locales. +func (b *Bundle) Languages() []Language { + b.mu.RLock() + defer b.mu.RUnlock() + out := make([]Language, len(b.languages)) + copy(out, b.languages) + return out +} + +func (b *Bundle) HasLanguage(code LanguageCode) bool { + b.mu.RLock() + defer b.mu.RUnlock() + _, ok := b.bundles[code] + return ok +} + +// BundleFor returns a copy of the full key->text map for one language. +func (b *Bundle) BundleFor(code LanguageCode) (map[string]string, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + bundle, ok := b.bundles[code] + if !ok { + return nil, fmt.Errorf("%w: %q", ErrUnsupportedLanguage, code) + } + out := make(map[string]string, len(bundle)) + for k, v := range bundle { + out[k] = v + } + return out, nil +} + +// Translate resolves key for lang, substituting args given as name/value +// pairs ("version", "1.2.3" replaces "{version}"). Unknown keys fall back to +// the default language, then to the key itself so a miss is visible in the UI. +func (b *Bundle) Translate(lang LanguageCode, key string, args ...string) string { + b.mu.RLock() + defer b.mu.RUnlock() + + if v, ok := b.bundles[lang][key]; ok { + return applyPlaceholders(v, args) + } + if lang != DefaultLanguage { + if v, ok := b.bundles[DefaultLanguage][key]; ok { + return applyPlaceholders(v, args) + } + } + return key +} + +// applyPlaceholders substitutes {name} in s using args as flat name/value +// pairs. An odd-length args drops the trailing item. +func applyPlaceholders(s string, args []string) string { + if len(args) == 0 { + return s + } + if len(args)%2 != 0 { + log.Debugf("i18n placeholder args not paired: %d items, last dropped", len(args)) + args = args[:len(args)-1] + } + for j := 0; j < len(args); j += 2 { + s = strings.ReplaceAll(s, "{"+args[j]+"}", args[j+1]) + } + return s +} + +func loadLocaleIndex(localesFS fs.FS) (*localeIndex, error) { + data, err := fs.ReadFile(localesFS, localeIndexFile) + if err != nil { + return nil, err + } + var idx localeIndex + if err := json.Unmarshal(data, &idx); err != nil { + return nil, fmt.Errorf("parse %s: %w", localeIndexFile, err) + } + if len(idx.Languages) == 0 { + return nil, errors.New("no languages declared") + } + return &idx, nil +} + +// bundleEntry is one translation key on disk; Description is Crowdin context, +// ignored at runtime. +type bundleEntry struct { + Message string `json:"message"` + Description string `json:"description,omitempty"` +} + +func loadBundle(localesFS fs.FS, code LanguageCode) (map[string]string, error) { + p := path.Join(string(code), commonBundleFile) + data, err := fs.ReadFile(localesFS, p) + if err != nil { + return nil, err + } + var entries map[string]bundleEntry + if err := json.Unmarshal(data, &entries); err != nil { + return nil, fmt.Errorf("parse %s: %w", p, err) + } + bundle := make(map[string]string, len(entries)) + for k, e := range entries { + bundle[k] = e.Message + } + return bundle, nil +} diff --git a/client/ui/i18n/bundle_test.go b/client/ui/i18n/bundle_test.go new file mode 100644 index 000000000..d0b0d24d7 --- /dev/null +++ b/client/ui/i18n/bundle_test.go @@ -0,0 +1,156 @@ +//go:build !android && !ios && !freebsd && !js + +package i18n + +import ( + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeLocales returns an in-memory FS that mirrors the real +// client/ui/i18n/locales layout (root-level _index.json plus +// /common.json bundles). Used by every Bundle test so we don't +// depend on the embedded production bundles staying stable. +func fakeLocales() fstest.MapFS { + return fstest.MapFS{ + "_index.json": {Data: []byte(`{ + "languages": [ + {"code": "en", "displayName": "English", "englishName": "English"}, + {"code": "hu", "displayName": "Magyar", "englishName": "Hungarian"} + ] + }`)}, + "en/common.json": {Data: []byte(`{ + "tray.menu.connect": {"message": "Connect", "description": "Tray menu item"}, + "tray.menu.installVersion": {"message": "Install version {version}"}, + "notify.update.body": {"message": "NetBird {version} is available."} + }`)}, + "hu/common.json": {Data: []byte(`{ + "tray.menu.connect": {"message": "Csatlakozás"}, + "tray.menu.installVersion": {"message": "{version} telepítése"} + }`)}, + } +} + +func TestBundle_LoadsAllLanguages(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + langs := b.Languages() + require.Len(t, langs, 2) + codes := []LanguageCode{langs[0].Code, langs[1].Code} + assert.ElementsMatch(t, []LanguageCode{"en", "hu"}, codes, "Languages should list every bundle that loaded") +} + +func TestBundle_TranslateLooksUpKey(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + assert.Equal(t, "Csatlakozás", b.Translate("hu", "tray.menu.connect")) + assert.Equal(t, "Connect", b.Translate("en", "tray.menu.connect")) +} + +func TestBundle_TranslateSubstitutesPlaceholders(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + assert.Equal(t, "Install version 1.2.3", + b.Translate("en", "tray.menu.installVersion", "version", "1.2.3"), + "placeholders should substitute by name") + assert.Equal(t, "1.2.3 telepítése", + b.Translate("hu", "tray.menu.installVersion", "version", "1.2.3")) +} + +func TestBundle_TranslateFallsBackToEnglish(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + // notify.update.body is missing from the hu bundle; English fallback + // applies so the user always sees a populated label rather than the + // raw key. + got := b.Translate("hu", "notify.update.body", "version", "9.9.9") + assert.Equal(t, "NetBird 9.9.9 is available.", got, "missing hu key should fall back to en bundle") +} + +func TestBundle_TranslateUnknownKeyReturnsKey(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + assert.Equal(t, "tray.missing", b.Translate("en", "tray.missing"), + "unknown key should return the key itself for debugability") +} + +func TestBundle_BundleForReturnsCopy(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + m, err := b.BundleFor("en") + require.NoError(t, err) + require.NotEmpty(t, m, "BundleFor should return populated map for known language") + + m["tray.menu.connect"] = "Mutated" + assert.Equal(t, "Connect", b.Translate("en", "tray.menu.connect"), + "BundleFor must return a copy, not the live map") +} + +func TestBundle_BundleForUnknownLanguage(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + _, err = b.BundleFor("xx") + assert.ErrorIs(t, err, ErrUnsupportedLanguage) +} + +func TestBundle_HasLanguage(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + assert.True(t, b.HasLanguage("en")) + assert.True(t, b.HasLanguage("hu")) + assert.False(t, b.HasLanguage("de")) +} + +func TestBundle_MissingDefaultBundleFails(t *testing.T) { + // Without an en bundle we have nothing to fall back to, so construction + // must hard-fail. Catches packaging accidents where someone drops the + // English locale. + fs := fstest.MapFS{ + "_index.json": {Data: []byte(`{"languages":[{"code":"hu","displayName":"Magyar","englishName":"Hungarian"}]}`)}, + "hu/common.json": {Data: []byte(`{"k":{"message":"v"}}`)}, + } + _, err := NewBundle(fs) + require.Error(t, err) + assert.Contains(t, err.Error(), "default language") +} + +func TestBundle_MissingBundleSkipsLanguage(t *testing.T) { + // A language declared in the index but missing its bundle file is + // dropped from Languages with a warning — adding a new language must + // be a two-step process (declare + ship), not declare-only. + fs := fstest.MapFS{ + "_index.json": {Data: []byte(`{"languages":[ + {"code":"en","displayName":"English","englishName":"English"}, + {"code":"de","displayName":"Deutsch","englishName":"German"} + ]}`)}, + "en/common.json": {Data: []byte(`{"k":{"message":"v"}}`)}, + } + b, err := NewBundle(fs) + require.NoError(t, err) + + langs := b.Languages() + require.Len(t, langs, 1) + assert.Equal(t, LanguageCode("en"), langs[0].Code, "language without a bundle file must be dropped") + assert.False(t, b.HasLanguage("de")) +} + +func TestBundle_OddPlaceholderArgsDoNotPanic(t *testing.T) { + b, err := NewBundle(fakeLocales()) + require.NoError(t, err) + + // Trailing dangling arg should be dropped, not panic — preserves UI + // stability when a caller passes an unpaired placeholder by mistake. + got := b.Translate("en", "tray.menu.installVersion", "version", "1.2.3", "extra") + assert.Equal(t, "Install version 1.2.3", got) +} diff --git a/client/ui/i18n/locales/_index.json b/client/ui/i18n/locales/_index.json new file mode 100644 index 000000000..58b5c484f --- /dev/null +++ b/client/ui/i18n/locales/_index.json @@ -0,0 +1,13 @@ +{ + "languages": [ + {"code": "en", "displayName": "English (US)", "englishName": "English (US)"}, + {"code": "de", "displayName": "Deutsch", "englishName": "German"}, + {"code": "hu", "displayName": "Magyar", "englishName": "Hungarian"}, + {"code": "ru", "displayName": "Русский", "englishName": "Russian"}, + {"code": "es", "displayName": "Español", "englishName": "Spanish"}, + {"code": "fr", "displayName": "Français", "englishName": "French"}, + {"code": "it", "displayName": "Italiano", "englishName": "Italian"}, + {"code": "pt", "displayName": "Português", "englishName": "Portuguese"}, + {"code": "zh-CN", "displayName": "简体中文", "englishName": "Simplified Chinese"} + ] +} diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json new file mode 100644 index 000000000..82f34ebf3 --- /dev/null +++ b/client/ui/i18n/locales/de/common.json @@ -0,0 +1,1331 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "Nicht verbunden" + }, + "tray.status.daemonUnavailable": { + "message": "Nicht aktiv" + }, + "tray.status.error": { + "message": "Fehler" + }, + "tray.status.connected": { + "message": "Verbunden" + }, + "tray.status.connecting": { + "message": "Wird verbunden" + }, + "tray.status.needsLogin": { + "message": "Anmeldung erforderlich" + }, + "tray.status.loginFailed": { + "message": "Anmeldung fehlgeschlagen" + }, + "tray.status.sessionExpired": { + "message": "Sitzung abgelaufen" + }, + "tray.session.expiresIn": { + "message": "Sitzung läuft ab in {remaining}" + }, + "tray.session.unit.lessThanMinute": { + "message": "weniger als einer Minute" + }, + "tray.session.unit.minute": { + "message": "1 Minute" + }, + "tray.session.unit.minutes": { + "message": "{count} Minuten" + }, + "tray.session.unit.hour": { + "message": "1 Stunde" + }, + "tray.session.unit.hours": { + "message": "{count} Stunden" + }, + "tray.session.unit.day": { + "message": "1 Tag" + }, + "tray.session.unit.days": { + "message": "{count} Tagen" + }, + "tray.menu.open": { + "message": "NetBird öffnen" + }, + "tray.menu.connect": { + "message": "Verbinden" + }, + "tray.menu.disconnect": { + "message": "Trennen" + }, + "tray.menu.exitNode": { + "message": "Exit Node" + }, + "tray.menu.networks": { + "message": "Ressourcen" + }, + "tray.menu.profiles": { + "message": "Profile" + }, + "tray.menu.manageProfiles": { + "message": "Profile verwalten" + }, + "tray.menu.settings": { + "message": "Einstellungen …" + }, + "tray.menu.debugBundle": { + "message": "Debug-Paket erstellen" + }, + "tray.menu.about": { + "message": "Hilfe & Support" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "Dokumentation" + }, + "tray.menu.troubleshoot": { + "message": "Fehlerbehebung" + }, + "tray.menu.downloadLatest": { + "message": "Neueste Version herunterladen" + }, + "tray.menu.installVersion": { + "message": "Version {version} installieren" + }, + "tray.menu.guiVersion": { + "message": "GUI: {version}" + }, + "tray.menu.daemonVersion": { + "message": "Daemon: {version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "NetBird beenden" + }, + "notify.daemonOutdated.title": { + "message": "NetBird-Dienst ist veraltet" + }, + "notify.daemonOutdated.body": { + "message": "Aktualisieren Sie den NetBird-Dienst, um diese App zu verwenden." + }, + "notify.update.title": { + "message": "NetBird-Update verfügbar" + }, + "notify.update.body": { + "message": "NetBird {version} ist verfügbar." + }, + "notify.update.enforcedSuffix": { + "message": " Ihr Administrator verlangt dieses Update." + }, + "notify.error.title": { + "message": "Fehler" + }, + "notify.error.connect": { + "message": "Verbindung fehlgeschlagen" + }, + "notify.error.disconnect": { + "message": "Trennen fehlgeschlagen" + }, + "notify.error.switchProfile": { + "message": "Wechsel zu {profile} fehlgeschlagen" + }, + "notify.error.exitNode": { + "message": "Exit Node {name} konnte nicht aktualisiert werden" + }, + "notify.sessionExpired.title": { + "message": "NetBird-Sitzung abgelaufen" + }, + "notify.sessionExpired.body": { + "message": "Ihre NetBird-Sitzung ist abgelaufen. Bitte melden Sie sich erneut an." + }, + "notify.sessionWarning.title": { + "message": "Sitzung läuft bald ab" + }, + "notify.sessionWarning.body": { + "message": "Ihre NetBird-Sitzung läuft in {remaining} ab. Klicken Sie auf Jetzt verlängern, um zu erneuern." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "Ihre NetBird-Sitzung läuft bald ab. Klicken Sie auf Jetzt verlängern, um zu erneuern." + }, + "notify.sessionWarning.extend": { + "message": "Jetzt verlängern" + }, + "notify.sessionWarning.dismiss": { + "message": "Verwerfen" + }, + "notify.sessionWarning.failed": { + "message": "NetBird-Sitzung konnte nicht verlängert werden" + }, + "notify.sessionWarning.successTitle": { + "message": "NetBird-Sitzung verlängert" + }, + "notify.sessionWarning.successBody": { + "message": "Ihre Sitzung wurde erneuert." + }, + "notify.sessionDeadlineRejected.title": { + "message": "Ungültige Sitzungsablaufzeit" + }, + "notify.sessionDeadlineRejected.body": { + "message": "Der Server hat eine ungültige Sitzungsablaufzeit übermittelt. Bitte melden Sie sich erneut an." + }, + "notify.mdm.policyApplied.title": { + "message": "NetBird-Einstellungen aktualisiert" + }, + "notify.mdm.policyApplied.body": { + "message": "Ihre NetBird-Konfiguration wurde durch Ihre IT-Richtlinie aktualisiert." + }, + "common.cancel": { + "message": "Abbrechen" + }, + "common.save": { + "message": "Speichern" + }, + "common.saveChanges": { + "message": "Änderungen speichern" + }, + "common.saving": { + "message": "Speichert…" + }, + "common.close": { + "message": "Schließen" + }, + "common.copy": { + "message": "Kopieren" + }, + "common.togglePasswordVisibility": { + "message": "Passwortsichtbarkeit umschalten" + }, + "common.increase": { + "message": "Erhöhen" + }, + "common.decrease": { + "message": "Verringern" + }, + "common.delete": { + "message": "Löschen" + }, + "common.create": { + "message": "Erstellen" + }, + "common.add": { + "message": "Hinzufügen" + }, + "common.remove": { + "message": "Entfernen" + }, + "common.refresh": { + "message": "Aktualisieren" + }, + "common.loading": { + "message": "Lädt…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "Keine Ergebnisse gefunden" + }, + "common.noResults.description": { + "message": "Es konnten keine Ergebnisse gefunden werden. Bitte versuchen Sie es mit einem anderen Suchbegriff oder ändern Sie Ihre Filter." + }, + "notConnected.title": { + "message": "Nicht verbunden" + }, + "notConnected.description": { + "message": "Verbinden Sie sich zuerst mit NetBird, um detaillierte Informationen zu Ihren Peers, Netzwerkressourcen und Exit Nodes einzusehen." + }, + "connect.status.disconnected": { + "message": "Nicht verbunden" + }, + "connect.status.connecting": { + "message": "Wird verbunden…" + }, + "connect.status.connected": { + "message": "Verbunden" + }, + "connect.status.disconnecting": { + "message": "Wird getrennt…" + }, + "connect.status.daemonUnavailable": { + "message": "Daemon nicht verfügbar" + }, + "connect.status.loginRequired": { + "message": "Anmeldung erforderlich" + }, + "connect.error.loginTitle": { + "message": "Anmeldung fehlgeschlagen" + }, + "connect.error.connectTitle": { + "message": "Verbindung fehlgeschlagen" + }, + "connect.error.disconnectTitle": { + "message": "Trennen fehlgeschlagen" + }, + "nav.peers.title": { + "message": "Peers" + }, + "nav.peers.description": { + "message": "{connected} von {total} verbunden" + }, + "nav.resources.title": { + "message": "Ressourcen" + }, + "nav.resources.description": { + "message": "{active} von {total} aktiv" + }, + "nav.exitNode.title": { + "message": "Exit Nodes" + }, + "nav.exitNode.none": { + "message": "Nicht aktiv" + }, + "nav.exitNode.using": { + "message": "Über {name}" + }, + "header.openSettings": { + "message": "Einstellungen öffnen" + }, + "header.togglePanel": { + "message": "Seitenleiste umschalten" + }, + "profile.selector.loading": { + "message": "Lädt…" + }, + "profile.selector.noProfile": { + "message": "Kein Profil" + }, + "profile.selector.searchPlaceholder": { + "message": "Profil nach Namen suchen…" + }, + "profile.selector.emptyTitle": { + "message": "Keine Profile gefunden" + }, + "profile.selector.emptyDescription": { + "message": "Versuchen Sie einen anderen Suchbegriff oder erstellen Sie ein neues Profil." + }, + "profile.selector.newProfile": { + "message": "Neues Profil" + }, + "profile.selector.moreOptions": { + "message": "Weitere Optionen" + }, + "profile.selector.deregister": { + "message": "Abmelden" + }, + "profile.selector.delete": { + "message": "Profil löschen" + }, + "profile.selector.switchTo": { + "message": "Zu diesem Profil wechseln" + }, + "profile.selector.edit": { + "message": "Bearbeiten" + }, + "profile.edit.title": { + "message": "Profil bearbeiten" + }, + "profile.edit.submit": { + "message": "Änderungen speichern" + }, + "profile.dialog.title": { + "message": "Neues Profil" + }, + "profile.dialog.nameLabel": { + "message": "Profilname" + }, + "profile.dialog.description": { + "message": "Legen Sie einen leicht erkennbaren Namen für Ihr Profil fest." + }, + "profile.dialog.placeholder": { + "message": "z. B. Arbeit" + }, + "profile.dialog.submit": { + "message": "Profil hinzufügen" + }, + "profile.dialog.required": { + "message": "Bitte geben Sie einen Profilnamen ein, z. B. Arbeit, Privat" + }, + "profile.dialog.managementHelp": { + "message": "NetBird Cloud oder Ihr eigener Server." + }, + "profile.dialog.urlUnreachable": { + "message": "Server nicht erreichbar. Überprüfen Sie die URL, oder fügen Sie das Profil trotzdem hinzu, wenn Sie sicher sind, dass sie korrekt ist." + }, + "header.menu.settings": { + "message": "Einstellungen …" + }, + "header.menu.defaultView": { + "message": "Standardansicht" + }, + "header.menu.advancedView": { + "message": "Erweiterte Ansicht" + }, + "header.menu.updateAvailable": { + "message": "Update verfügbar" + }, + "header.menu.open": { + "message": "Menü öffnen" + }, + "header.profile.switch": { + "message": "Profil wechseln" + }, + "connect.toggle.label": { + "message": "NetBird-Verbindung umschalten" + }, + "connect.localIp.label": { + "message": "Lokale IP-Adressen" + }, + "common.search": { + "message": "Suchen" + }, + "common.filter": { + "message": "Filtern" + }, + "exitNodes.dropdown.trigger": { + "message": "Exit-Node auswählen" + }, + "peers.row.label": { + "message": "Details öffnen für {name}, {status}" + }, + "peers.dialog.title": { + "message": "Peer-Details" + }, + "networks.row.toggle": { + "message": "{name} umschalten" + }, + "networks.bulk.label": { + "message": "Alle sichtbaren Ressourcen umschalten" + }, + "settings.nav.label": { + "message": "Einstellungsbereiche" + }, + "profile.switch.title": { + "message": "Zu Profil \"{name}\" wechseln?" + }, + "profile.switch.message": { + "message": "Sind Sie sicher, dass Sie das Profil wechseln möchten?\nIhr aktuelles Profil wird getrennt." + }, + "profile.switch.confirm": { + "message": "Bestätigen" + }, + "profile.deregister.title": { + "message": "Profil \"{name}\" abmelden?" + }, + "profile.deregister.message": { + "message": "Sind Sie sicher, dass Sie dieses Profil abmelden möchten?\nSie müssen sich erneut anmelden, um es zu nutzen." + }, + "profile.deregister.confirm": { + "message": "Abmelden" + }, + "profile.delete.title": { + "message": "Profil \"{name}\" löschen?" + }, + "profile.delete.message": { + "message": "Sind Sie sicher, dass Sie dieses Profil löschen möchten?\nDiese Aktion kann nicht rückgängig gemacht werden." + }, + "profile.delete.disabledActive": { + "message": "Aktive Profile können nicht gelöscht werden. Wechseln Sie zu einem anderen Profil, bevor Sie dieses löschen." + }, + "profile.delete.disabledDefault": { + "message": "Das Standardprofil kann nicht gelöscht werden." + }, + "profile.error.switchTitle": { + "message": "Profilwechsel fehlgeschlagen" + }, + "profile.error.deregisterTitle": { + "message": "Abmeldung fehlgeschlagen" + }, + "profile.error.deleteTitle": { + "message": "Löschen des Profils fehlgeschlagen" + }, + "profile.error.createTitle": { + "message": "Erstellen des Profils fehlgeschlagen" + }, + "profile.error.editTitle": { + "message": "Bearbeiten des Profils fehlgeschlagen" + }, + "profile.error.loadTitle": { + "message": "Laden der Profile fehlgeschlagen" + }, + "profile.dropdown.activeProfile": { + "message": "Aktives Profil" + }, + "profile.dropdown.switchProfile": { + "message": "Profil wechseln" + }, + "profile.dropdown.noEmail": { + "message": "Andere" + }, + "profile.dropdown.addProfile": { + "message": "Profil hinzufügen" + }, + "profile.dropdown.manageProfiles": { + "message": "Profile verwalten" + }, + "profile.dropdown.settings": { + "message": "Einstellungen" + }, + "settings.profiles.section.profiles": { + "message": "Profile" + }, + "settings.profiles.intro": { + "message": "Verwalten Sie mehrere NetBird-Profile parallel, zum Beispiel berufliche und private Konten oder verschiedene Management-Server. Fügen Sie unten Profile hinzu, melden Sie sie ab oder löschen Sie sie." + }, + "settings.profiles.addProfile": { + "message": "Profil hinzufügen" + }, + "settings.profiles.active": { + "message": "Aktiv" + }, + "settings.profiles.emptyTitle": { + "message": "Keine Profile" + }, + "settings.profiles.emptyDescription": { + "message": "Erstellen Sie ein Profil, um sich mit einem NetBird-Management-Server zu verbinden." + }, + "settings.error.loadTitle": { + "message": "Laden der Einstellungen fehlgeschlagen" + }, + "settings.error.saveTitle": { + "message": "Speichern der Einstellungen fehlgeschlagen" + }, + "settings.error.debugBundleTitle": { + "message": "Debug-Paket fehlgeschlagen" + }, + "settings.tabs.general": { + "message": "Allgemein" + }, + "settings.tabs.network": { + "message": "Netzwerk" + }, + "settings.tabs.security": { + "message": "Sicherheit" + }, + "settings.tabs.profiles": { + "message": "Profile" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "Erweitert" + }, + "settings.tabs.troubleshooting": { + "message": "Fehlerbehebung" + }, + "settings.tabs.about": { + "message": "Über" + }, + "settings.tabs.updateAvailable": { + "message": "Update verfügbar" + }, + "settings.general.section.general": { + "message": "Allgemein" + }, + "settings.general.section.connection": { + "message": "Verbindung" + }, + "settings.general.connectOnStartup.label": { + "message": "Beim Start verbinden" + }, + "settings.general.connectOnStartup.help": { + "message": "Beim Start des Dienstes automatisch eine Verbindung herstellen." + }, + "settings.general.notifications.label": { + "message": "Desktop-Benachrichtigungen" + }, + "settings.general.notifications.help": { + "message": "Desktop-Benachrichtigungen für neue Updates und Verbindungsereignisse anzeigen." + }, + "settings.general.autostart.label": { + "message": "NetBird-UI beim Anmelden starten" + }, + "settings.general.autostart.help": { + "message": "Die NetBird-Oberfläche beim Anmelden automatisch starten. Dies betrifft nur die grafische Oberfläche, nicht den Hintergrunddienst." + }, + "settings.general.autostart.errorTitle": { + "message": "Ändern des Autostarts fehlgeschlagen" + }, + "settings.general.language.label": { + "message": "Anzeigesprache" + }, + "settings.general.language.help": { + "message": "Wählen Sie die Sprache der NetBird-Oberfläche." + }, + "settings.general.language.search": { + "message": "Sprache suchen…" + }, + "settings.general.language.empty": { + "message": "Keine Sprachen gefunden." + }, + "settings.general.management.label": { + "message": "Management-Server" + }, + "settings.general.management.help": { + "message": "Mit NetBird Cloud oder Ihrem eigenen self-hosted Management-Server verbinden. Änderungen lösen eine Neuverbindung des Clients aus." + }, + "settings.general.management.cloud": { + "message": "Cloud" + }, + "settings.general.management.selfHosted": { + "message": "Self-hosted" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "Bitte geben Sie eine gültige URL ein, z. B. https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "Server nicht erreichbar. Überprüfen Sie die URL, oder speichern Sie trotzdem, wenn Sie sicher sind, dass sie korrekt ist." + }, + "settings.general.management.switchCloudTitle": { + "message": "Zu NetBird Cloud wechseln?" + }, + "settings.general.management.switchCloudMessage": { + "message": "Dies trennt die Verbindung zu Ihrem self-hosted Server.\nMöglicherweise müssen Sie sich erneut anmelden." + }, + "settings.general.management.switchCloudConfirm": { + "message": "Zu Cloud wechseln" + }, + "settings.network.section.connectivity": { + "message": "Konnektivität" + }, + "settings.network.section.routingDns": { + "message": "Routing & DNS" + }, + "settings.network.lazy.label": { + "message": "Lazy-Verbindungen" + }, + "settings.network.lazy.help": { + "message": "Statt durchgehend aktive Verbindungen zu halten, aktiviert NetBird sie bei Bedarf anhand von Aktivität oder Signalisierung." + }, + "settings.network.monitor.label": { + "message": "Bei Netzwerkwechsel neu verbinden" + }, + "settings.network.monitor.help": { + "message": "Das Netzwerk überwachen und bei Änderungen (z. B. WLAN-Wechsel, Ethernet-Änderungen oder Rückkehr aus dem Ruhezustand) automatisch neu verbinden." + }, + "settings.network.dns.label": { + "message": "DNS aktivieren" + }, + "settings.network.dns.help": { + "message": "NetBird-verwaltete DNS-Einstellungen auf den Host-Resolver anwenden." + }, + "settings.network.clientRoutes.label": { + "message": "Client-Routen aktivieren" + }, + "settings.network.clientRoutes.help": { + "message": "Routen von anderen Peers übernehmen, um deren Netzwerke zu erreichen." + }, + "settings.network.serverRoutes.label": { + "message": "Server-Routen aktivieren" + }, + "settings.network.serverRoutes.help": { + "message": "Lokale Routen dieses Hosts an andere Peers ankündigen." + }, + "settings.network.ipv6.label": { + "message": "IPv6 aktivieren" + }, + "settings.network.ipv6.help": { + "message": "IPv6-Adressierung für das NetBird-Overlay-Netzwerk verwenden." + }, + "settings.security.section.firewall": { + "message": "Firewall" + }, + "settings.security.section.encryption": { + "message": "Verschlüsselung" + }, + "settings.security.blockInbound.label": { + "message": "Eingehenden Verkehr blockieren" + }, + "settings.security.blockInbound.help": { + "message": "Unaufgeforderte Verbindungen von Peers zu diesem Gerät und den von ihm gerouteten Netzwerken ablehnen. Ausgehender Verkehr ist nicht betroffen." + }, + "settings.security.blockLan.label": { + "message": "LAN-Zugriff blockieren" + }, + "settings.security.blockLan.help": { + "message": "Verhindert, dass Peers Ihr lokales Netzwerk oder dessen Geräte erreichen, wenn dieses Gerät deren Verkehr routet." + }, + "settings.security.rosenpass.label": { + "message": "Quantenresistenz aktivieren" + }, + "settings.security.rosenpass.help": { + "message": "Einen Post-Quanten-Schlüsselaustausch über Rosenpass zusätzlich zu WireGuard® hinzufügen." + }, + "settings.security.rosenpassPermissive.label": { + "message": "Permissiven Modus aktivieren" + }, + "settings.security.rosenpassPermissive.help": { + "message": "Verbindungen zu Peers ohne Quantenresistenz-Unterstützung erlauben." + }, + "settings.ssh.section.server": { + "message": "Server" + }, + "settings.ssh.section.capabilities": { + "message": "Funktionen" + }, + "settings.ssh.section.authentication": { + "message": "Authentifizierung" + }, + "settings.ssh.server.label": { + "message": "SSH-Server aktivieren" + }, + "settings.ssh.server.help": { + "message": "Den NetBird SSH-Server auf diesem Host ausführen, damit andere Peers sich verbinden können." + }, + "settings.ssh.root.label": { + "message": "Root-Login erlauben" + }, + "settings.ssh.root.help": { + "message": "Peers dürfen sich als root anmelden. Deaktivieren, um ein nicht-privilegiertes Konto zu erfordern." + }, + "settings.ssh.sftp.label": { + "message": "SFTP erlauben" + }, + "settings.ssh.sftp.help": { + "message": "Dateien sicher über native SFTP- oder SCP-Clients übertragen." + }, + "settings.ssh.localForward.label": { + "message": "Lokale Portweiterleitung" + }, + "settings.ssh.localForward.help": { + "message": "Verbundene Peers können lokale Ports zu von diesem Host erreichbaren Diensten tunneln." + }, + "settings.ssh.remoteForward.label": { + "message": "Remote-Portweiterleitung" + }, + "settings.ssh.remoteForward.help": { + "message": "Verbundene Peers können Ports dieses Hosts an ihren eigenen Rechner weitergeben." + }, + "settings.ssh.jwt.label": { + "message": "JWT-Authentifizierung aktivieren" + }, + "settings.ssh.jwt.help": { + "message": "Jede SSH-Sitzung gegen Ihren IdP für Identität und Audit prüfen. Deaktivieren, um sich nur auf Netzwerk-ACL-Richtlinien zu verlassen — sinnvoll, wenn kein IdP verfügbar ist." + }, + "settings.ssh.jwtTtl.label": { + "message": "JWT-Cache-TTL" + }, + "settings.ssh.jwtTtl.help": { + "message": "Wie lange dieser Client ein JWT zwischenspeichert, bevor bei ausgehenden SSH-Verbindungen erneut nachgefragt wird. Auf 0 setzen, um den Cache zu deaktivieren und bei jeder Verbindung zu authentifizieren." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "Sekunde(n)" + }, + "settings.advanced.section.interface": { + "message": "Schnittstelle" + }, + "settings.advanced.section.security": { + "message": "Sicherheit" + }, + "settings.advanced.interfaceName.label": { + "message": "Name" + }, + "settings.advanced.interfaceName.error": { + "message": "Verwenden Sie 1–15 Buchstaben, Ziffern, Punkte, Bindestriche oder Unterstriche." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "Muss mit „utun“ und einer Zahl beginnen (z. B. utun100)." + }, + "settings.advanced.port.label": { + "message": "Port" + }, + "settings.advanced.port.error": { + "message": "Geben Sie einen Port zwischen {min} und {max} ein." + }, + "settings.advanced.port.help": { + "message": "Wenn auf 0 gesetzt, wird ein zufälliger freier Port verwendet." + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "Geben Sie einen MTU-Wert zwischen {min} und {max} ein." + }, + "settings.advanced.psk.label": { + "message": "Pre-shared Key" + }, + "settings.advanced.psk.help": { + "message": "Optionaler WireGuard-PSK für zusätzliche symmetrische Verschlüsselung. Nicht identisch mit einem NetBird Setup-Key. Sie kommunizieren nur mit Peers, die denselben Pre-shared Key verwenden." + }, + "settings.troubleshooting.section.title": { + "message": "Debug-Paket" + }, + "settings.troubleshooting.anonymize.label": { + "message": "Sensible Informationen anonymisieren" + }, + "settings.troubleshooting.anonymize.help": { + "message": "Versteckt öffentliche IP-Adressen und nicht-NetBird-Domains in Logs." + }, + "settings.troubleshooting.systemInfo.label": { + "message": "Systeminformationen einschließen" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "OS, Kernel, Netzwerkschnittstellen und Routing-Tabellen einschließen." + }, + "settings.troubleshooting.upload.label": { + "message": "Paket an NetBird-Server hochladen" + }, + "settings.troubleshooting.upload.help": { + "message": "Gibt einen Upload-Schlüssel zurück, den Sie mit dem NetBird-Support teilen können." + }, + "settings.troubleshooting.trace.label": { + "message": "Trace-Logs aktivieren" + }, + "settings.troubleshooting.trace.help": { + "message": "Hebt das Log-Level auf TRACE an und stellt es danach wieder her." + }, + "settings.troubleshooting.capture.label": { + "message": "Aufzeichnungssitzung" + }, + "settings.troubleshooting.capture.help": { + "message": "Stellt die Verbindung neu her und wartet, damit Sie das Problem reproduzieren können." + }, + "settings.troubleshooting.packets.label": { + "message": "Netzwerkpakete aufzeichnen" + }, + "settings.troubleshooting.packets.help": { + "message": "Speichert eine .pcap-Datei des Netzwerkverkehrs während der Aufzeichnung." + }, + "settings.troubleshooting.duration.label": { + "message": "Aufzeichnungsdauer" + }, + "settings.troubleshooting.duration.help": { + "message": "Wie lange die Aufzeichnungssitzung läuft." + }, + "settings.troubleshooting.duration.suffix": { + "message": "Minute(n)" + }, + "settings.troubleshooting.create": { + "message": "Debug-Paket erstellen" + }, + "settings.troubleshooting.progress.description": { + "message": "Logs, Systemdetails und Verbindungszustand werden gesammelt. Dies dauert in der Regel einen Moment — lassen Sie dieses Fenster geöffnet, bis es abgeschlossen ist." + }, + "settings.troubleshooting.cancelling": { + "message": "Wird abgebrochen…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "Debug-Paket erfolgreich hochgeladen!" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "Paket gespeichert" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "Teilen Sie den unten angezeigten Upload-Schlüssel mit dem NetBird-Support. Eine lokale Kopie wurde ebenfalls auf Ihrem Gerät gespeichert." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "Ihr Debug-Paket wurde lokal gespeichert." + }, + "settings.troubleshooting.done.copyKey": { + "message": "Schlüssel kopieren" + }, + "settings.troubleshooting.done.openFolder": { + "message": "Ordner öffnen" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "Speicherort öffnen" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "Upload fehlgeschlagen: {reason} Das Paket wurde trotzdem lokal gespeichert." + }, + "settings.troubleshooting.uploadFailed": { + "message": "Upload fehlgeschlagen. Das Paket wurde trotzdem lokal gespeichert." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "NetBird wird neu verbunden…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "Debug-Logs werden erfasst" + }, + "settings.troubleshooting.stage.bundling": { + "message": "Debug-Paket wird erstellt…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "Wird zu NetBird hochgeladen…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "Wird abgebrochen…" + }, + "settings.about.client": { + "message": "NetBird Client v{version}" + }, + "settings.about.clientName": { + "message": "NetBird Client" + }, + "settings.about.development": { + "message": "[Entwicklung]" + }, + "settings.about.gui": { + "message": "GUI v{version}" + }, + "settings.about.guiName": { + "message": "GUI" + }, + "settings.about.copyright": { + "message": "© {year} NetBird. Alle Rechte vorbehalten." + }, + "settings.about.links.imprint": { + "message": "Impressum" + }, + "settings.about.links.privacy": { + "message": "Datenschutz" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "Nutzungsbedingungen" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "Forum" + }, + "settings.about.community.documentation": { + "message": "Dokumentation" + }, + "settings.about.community.feedback": { + "message": "Feedback" + }, + "update.banner.message": { + "message": "NetBird {version} ist installationsbereit." + }, + "update.banner.later": { + "message": "Später" + }, + "update.banner.installNow": { + "message": "Jetzt installieren" + }, + "update.card.versionAvailableDownload": { + "message": "Version {version} ist zum Herunterladen verfügbar." + }, + "update.card.versionAvailableInstall": { + "message": "Version {version} ist zur Installation verfügbar." + }, + "update.card.whatsNew": { + "message": "Was ist neu?" + }, + "update.card.installNow": { + "message": "Jetzt installieren" + }, + "update.card.getInstaller": { + "message": "Herunterladen" + }, + "update.card.autoCheckInterval": { + "message": "NetBird sucht im Hintergrund nach Updates." + }, + "update.card.changelog": { + "message": "Changelog" + }, + "update.card.onLatestVersion": { + "message": "Sie verwenden die neueste Version" + }, + "update.header.tooltip": { + "message": "Update verfügbar" + }, + "update.overlay.updatingVersion": { + "message": "NetBird wird auf v{version} aktualisiert" + }, + "update.overlay.updating": { + "message": "NetBird wird aktualisiert" + }, + "update.overlay.description": { + "message": "Eine neuere Version ist verfügbar und wird installiert. NetBird startet nach Abschluss des Updates automatisch neu." + }, + "update.overlay.error.timeoutTitle": { + "message": "Update dauert zu lange" + }, + "update.overlay.error.timeoutDescription": { + "message": "Die Installation von {target} hat zu lange gedauert und wurde nicht abgeschlossen." + }, + "update.overlay.error.canceledTitle": { + "message": "Update wurde abgebrochen" + }, + "update.overlay.error.canceledDescription": { + "message": "Das Update auf {target} wurde vor dem Abschluss abgebrochen." + }, + "update.overlay.error.failTitle": { + "message": "Update konnte nicht installiert werden" + }, + "update.overlay.error.failDescription": { + "message": "{target} konnte nicht installiert werden." + }, + "update.overlay.error.unknownMessage": { + "message": "unbekannter Fehler" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "die neue Version" + }, + "update.error.loadStateTitle": { + "message": "Laden des Update-Status fehlgeschlagen" + }, + "update.error.triggerTitle": { + "message": "Update-Start fehlgeschlagen" + }, + "update.page.versionLine": { + "message": "Client wird aktualisiert auf: {version}." + }, + "update.page.versionLineGeneric": { + "message": "Client wird aktualisiert." + }, + "update.page.outdated": { + "message": "Ihre Client-Version ist älter als die im Management eingestellte Auto-Update-Version." + }, + "update.page.status.running": { + "message": "Wird aktualisiert" + }, + "update.page.status.timeout": { + "message": "Zeitüberschreitung beim Update. Bitte erneut versuchen." + }, + "update.page.status.canceled": { + "message": "Update abgebrochen." + }, + "update.page.status.failed": { + "message": "Update fehlgeschlagen: {message}" + }, + "update.page.status.unknownError": { + "message": "unbekannter Update-Fehler" + }, + "update.page.failedTitle": { + "message": "Update fehlgeschlagen" + }, + "update.page.timeoutMessage": { + "message": "Zeitüberschreitung beim Update." + }, + "update.page.dontClose": { + "message": "Bitte schließen Sie dieses Fenster nicht." + }, + "update.page.updating": { + "message": "Wird aktualisiert…" + }, + "update.page.complete": { + "message": "Update abgeschlossen" + }, + "update.page.failed": { + "message": "Update fehlgeschlagen" + }, + "window.title.settings": { + "message": "Einstellungen" + }, + "window.title.signIn": { + "message": "Anmeldung" + }, + "window.title.sessionExpiration": { + "message": "Sitzung läuft ab" + }, + "window.title.updating": { + "message": "Aktualisierung" + }, + "window.title.welcome": { + "message": "Willkommen bei NetBird" + }, + "window.title.error": { + "message": "Fehler" + }, + "welcome.title": { + "message": "Suchen Sie NetBird in der Taskleiste" + }, + "welcome.description": { + "message": "NetBird läuft in Ihrer Taskleiste. Klicken Sie auf das Symbol, um sich zu verbinden, Profile zu wechseln oder die Einstellungen zu öffnen." + }, + "welcome.continue": { + "message": "Weiter" + }, + "welcome.back": { + "message": "Zurück" + }, + "welcome.management.title": { + "message": "NetBird einrichten" + }, + "welcome.management.description": { + "message": "Klicken Sie auf „Weiter“, um loszulegen, oder wählen Sie „Self-hosted“, wenn Sie einen eigenen NetBird-Server haben." + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud" + }, + "welcome.management.cloud.description": { + "message": "Nutzen Sie unseren gehosteten Dienst. Keine Einrichtung nötig." + }, + "welcome.management.selfHosted.title": { + "message": "Self-hosted" + }, + "welcome.management.selfHosted.description": { + "message": "Verbindung zu Ihrem eigenen Management-Server." + }, + "welcome.management.urlLabel": { + "message": "URL des Management-Servers" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "Bitte geben Sie eine gültige URL ein, z. B. https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "Server nicht erreichbar. Überprüfen Sie die URL oder Ihr Netzwerk und fahren Sie fort, wenn Sie sicher sind, dass sie korrekt ist." + }, + "welcome.management.checking": { + "message": "Wird geprüft …" + }, + "browserLogin.title": { + "message": "Anmeldung im Browser abschließen" + }, + "browserLogin.notSeeing": { + "message": "Sehen Sie den Browser-Tab nicht?" + }, + "browserLogin.tryAgain": { + "message": "Erneut versuchen" + }, + "browserLogin.openFailedTitle": { + "message": "Browser konnte nicht geöffnet werden" + }, + "sessionExpiration.title": { + "message": "Sitzung läuft bald ab" + }, + "sessionExpiration.titleLater": { + "message": "Ihre Sitzung läuft ab" + }, + "sessionExpiration.description": { + "message": "Dieses Gerät wird bald getrennt. Browser-Anmeldung zum Erneuern erforderlich." + }, + "sessionExpiration.descriptionLater": { + "message": "Eine Browser-Anmeldung hält dieses Gerät mit Ihrem Netzwerk verbunden." + }, + "sessionExpiration.stay": { + "message": "Sitzung erneuern" + }, + "sessionExpiration.authenticate": { + "message": "Anmelden" + }, + "sessionExpiration.logout": { + "message": "Abmelden" + }, + "sessionExpiration.expired": { + "message": "Sitzung abgelaufen" + }, + "sessionExpiration.expiredDescription": { + "message": "Gerät getrennt. Mit Browser-Anmeldung authentifizieren, um erneut zu verbinden." + }, + "sessionExpiration.close": { + "message": "Schließen" + }, + "sessionExpiration.extendFailedTitle": { + "message": "Sitzungsverlängerung fehlgeschlagen" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "Abmeldung fehlgeschlagen" + }, + "peers.search.placeholder": { + "message": "Nach Name oder IP suchen" + }, + "peers.filter.all": { + "message": "Alle" + }, + "peers.filter.online": { + "message": "Online" + }, + "peers.filter.offline": { + "message": "Offline" + }, + "peers.empty.title": { + "message": "Keine Peers verfügbar" + }, + "peers.empty.description": { + "message": "Sie haben entweder keine Peers verfügbar oder keinen Zugriff auf einen davon." + }, + "peers.details.domain": { + "message": "Domain" + }, + "peers.details.netbirdIp": { + "message": "NetBird-IP" + }, + "peers.details.netbirdIpv6": { + "message": "NetBird-IPv6" + }, + "peers.details.publicKey": { + "message": "Öffentlicher Schlüssel" + }, + "peers.details.connection": { + "message": "Verbindung" + }, + "peers.details.latency": { + "message": "Latenz" + }, + "peers.details.lastHandshake": { + "message": "Letzter Handshake" + }, + "peers.details.statusSince": { + "message": "Letzte Verbindungsaktualisierung" + }, + "peers.details.bytes": { + "message": "Bytes" + }, + "peers.details.bytesSent": { + "message": "Gesendet" + }, + "peers.details.bytesReceived": { + "message": "Empfangen" + }, + "peers.details.localIce": { + "message": "Lokales ICE" + }, + "peers.details.remoteIce": { + "message": "Remote ICE" + }, + "peers.details.never": { + "message": "Nie" + }, + "peers.details.justNow": { + "message": "Gerade eben" + }, + "peers.details.refresh": { + "message": "Aktualisieren" + }, + "peers.status.connected": { + "message": "Verbunden" + }, + "peers.status.connecting": { + "message": "Wird verbunden" + }, + "peers.status.disconnected": { + "message": "Nicht verbunden" + }, + "peers.details.relayAddress": { + "message": "Relay" + }, + "peers.details.networks": { + "message": "Ressourcen" + }, + "peers.details.relayed": { + "message": "Relayed" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "Rosenpass aktiviert" + }, + "networks.search.placeholder": { + "message": "Nach Netzwerk oder Domain suchen" + }, + "networks.filter.all": { + "message": "Alle" + }, + "networks.filter.active": { + "message": "Aktiv" + }, + "networks.filter.overlapping": { + "message": "Überlappend" + }, + "networks.empty.title": { + "message": "Keine Ressourcen verfügbar" + }, + "networks.empty.description": { + "message": "Sie haben entweder keine Netzwerkressourcen verfügbar oder keinen Zugriff auf eine davon." + }, + "networks.selected": { + "message": "Ausgewählt" + }, + "networks.unselected": { + "message": "Nicht ausgewählt" + }, + "networks.ips.heading": { + "message": "Aufgelöste IPs" + }, + "networks.bulk.selectionCount": { + "message": "{selected} von {total} aktiv" + }, + "networks.bulk.enableAll": { + "message": "Alle aktivieren" + }, + "networks.bulk.disableAll": { + "message": "Alle deaktivieren" + }, + "exitNodes.search.placeholder": { + "message": "Exit Nodes suchen" + }, + "exitNodes.none": { + "message": "Keiner" + }, + "exitNodes.empty.title": { + "message": "Keine Exit Nodes verfügbar" + }, + "exitNodes.empty.description": { + "message": "Für diesen Peer wurden keine Exit Nodes freigegeben." + }, + "exitNodes.card.title": { + "message": "Exit Node" + }, + "exitNodes.card.statusActive": { + "message": "Aktiv" + }, + "exitNodes.card.statusInactive": { + "message": "Inaktiv" + }, + "exitNodes.dropdown.noneTitle": { + "message": "Keiner" + }, + "exitNodes.dropdown.noneDescription": { + "message": "Direkte Verbindung ohne Exit Node" + }, + "quickActions.connect": { + "message": "Verbinden" + }, + "quickActions.disconnect": { + "message": "Trennen" + }, + "daemon.unavailable.title": { + "message": "NetBird-Dienst läuft nicht" + }, + "daemon.unavailable.description": { + "message": "Die App stellt automatisch die Verbindung wieder her, sobald der Dienst läuft." + }, + "daemon.unavailable.docsLink": { + "message": "Dokumentation" + }, + "daemon.outdated.title": { + "message": "NetBird-Dienst ist veraltet" + }, + "daemon.outdated.description": { + "message": "Aktualisieren Sie den NetBird-Dienst, um diese App zu verwenden." + }, + "error.jwt_clock_skew": { + "message": "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": { + "message": "Ihr Anmeldetoken ist abgelaufen. Bitte melden Sie sich erneut an." + }, + "error.jwt_signature_invalid": { + "message": "Anmeldung fehlgeschlagen: Die Token-Signatur ist ungültig. Bitte wenden Sie sich an Ihren Administrator." + }, + "error.session_expired": { + "message": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an." + }, + "error.invalid_setup_key": { + "message": "Der Setup-Key fehlt oder ist ungültig." + }, + "error.permission_denied": { + "message": "Die Anmeldung wurde vom Server abgelehnt." + }, + "error.daemon_unreachable": { + "message": "Der NetBird-Dienst antwortet nicht. Bitte prüfen Sie, ob der Dienst läuft." + }, + "error.unknown": { + "message": "Vorgang fehlgeschlagen." + } +} diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json new file mode 100644 index 000000000..1553e7f2f --- /dev/null +++ b/client/ui/i18n/locales/en/common.json @@ -0,0 +1,1774 @@ +{ + "tray.tooltip": { + "message": "NetBird", + "description": "Hover tooltip on the system-tray / menu-bar icon. Brand name — do not translate." + }, + "tray.status.disconnected": { + "message": "Disconnected", + "description": "Connection status surfaced through the tray icon: the client is not connected to the network." + }, + "tray.status.daemonUnavailable": { + "message": "Not running", + "description": "Tray status: the background NetBird service (daemon) is not running." + }, + "tray.status.error": { + "message": "Error", + "description": "Tray status: the client is in an error state." + }, + "tray.status.connected": { + "message": "Connected", + "description": "Tray status: connected to the NetBird network." + }, + "tray.status.connecting": { + "message": "Connecting", + "description": "Tray status: a connection is being established." + }, + "tray.status.needsLogin": { + "message": "Login required", + "description": "Tray status: the user must sign in before connecting." + }, + "tray.status.loginFailed": { + "message": "Login failed", + "description": "Tray status: the last sign-in attempt failed." + }, + "tray.status.sessionExpired": { + "message": "Session expired", + "description": "Tray status: the authenticated session expired; the user must sign in again." + }, + "tray.session.expiresIn": { + "message": "Session expires in {remaining}", + "description": "Tray row showing time left before the session expires. {remaining} is a human-readable duration such as '5 minutes', built from the tray.session.unit.* strings. Keep {remaining} unchanged." + }, + "tray.session.unit.lessThanMinute": { + "message": "less than a minute", + "description": "Duration fragment substituted into {remaining} (see tray.session.expiresIn). Used when under one minute remains." + }, + "tray.session.unit.minute": { + "message": "1 minute", + "description": "Duration fragment for exactly one minute, substituted into {remaining}." + }, + "tray.session.unit.minutes": { + "message": "{count} minutes", + "description": "Duration fragment for several minutes, substituted into {remaining}. {count} is the number of minutes; keep {count}." + }, + "tray.session.unit.hour": { + "message": "1 hour", + "description": "Duration fragment for exactly one hour, substituted into {remaining}." + }, + "tray.session.unit.hours": { + "message": "{count} hours", + "description": "Duration fragment for several hours. {count} is the number of hours; keep {count}." + }, + "tray.session.unit.day": { + "message": "1 day", + "description": "Duration fragment for exactly one day, substituted into {remaining}." + }, + "tray.session.unit.days": { + "message": "{count} days", + "description": "Duration fragment for several days. {count} is the number of days; keep {count}." + }, + "tray.menu.open": { + "message": "Open NetBird", + "description": "Tray menu item that opens the main NetBird window. Keep short." + }, + "tray.menu.connect": { + "message": "Connect", + "description": "Tray menu item that connects to the network. Keep short." + }, + "tray.menu.disconnect": { + "message": "Disconnect", + "description": "Tray menu item that disconnects from the network. Keep short." + }, + "tray.menu.exitNode": { + "message": "Exit Node", + "description": "Tray submenu title for choosing an exit node (route all traffic through another peer)." + }, + "tray.menu.networks": { + "message": "Resources", + "description": "Tray submenu title listing network resources the user can reach. Labelled 'Resources' in the UI even though the key says networks." + }, + "tray.menu.profiles": { + "message": "Profiles", + "description": "Tray submenu title listing connection profiles." + }, + "tray.menu.manageProfiles": { + "message": "Manage Profiles", + "description": "Tray menu item that opens Settings → Profiles." + }, + "tray.menu.settings": { + "message": "Settings...", + "description": "Tray menu item that opens the Settings window. The trailing '...' signals that a window opens; keep it." + }, + "tray.menu.debugBundle": { + "message": "Create Debug Bundle", + "description": "Tray menu item that generates a diagnostic log bundle for support." + }, + "tray.menu.about": { + "message": "Help & Support", + "description": "Tray submenu title for help and support links." + }, + "tray.menu.github": { + "message": "GitHub", + "description": "Tray menu link to the GitHub repository. Brand name — do not translate." + }, + "tray.menu.documentation": { + "message": "Documentation", + "description": "Tray menu link to the online documentation." + }, + "tray.menu.troubleshoot": { + "message": "Troubleshoot", + "description": "Tray menu link to troubleshooting help." + }, + "tray.menu.downloadLatest": { + "message": "Download latest version", + "description": "Tray menu item to download the latest available version." + }, + "tray.menu.installVersion": { + "message": "Install version {version}", + "description": "Tray menu item to install a specific update. {version} is a version number like 0.30.1; keep {version}." + }, + "tray.menu.guiVersion": { + "message": "GUI: {version}", + "description": "Tray menu row showing the installed UI version. 'GUI' = the graphical app. {version} is a version number; keep it." + }, + "tray.menu.daemonVersion": { + "message": "Daemon: {version}", + "description": "Tray menu row showing the background-service version. 'Daemon' = the background NetBird service. {version} is a version number; keep it." + }, + "tray.menu.versionUnknown": { + "message": "—", + "description": "Placeholder shown in version rows when the version can't be determined. It is an em dash — leave as-is." + }, + "tray.menu.quit": { + "message": "Quit NetBird", + "description": "Tray menu item that fully exits the app and removes the tray icon. Keep short." + }, + "notify.daemonOutdated.title": { + "message": "NetBird service is outdated", + "description": "Title of the OS desktop notification shown when the running daemon is too old for this UI." + }, + "notify.daemonOutdated.body": { + "message": "Update the NetBird service to use this app.", + "description": "Body of the desktop notification telling the user to upgrade the daemon." + }, + "notify.update.title": { + "message": "NetBird update available", + "description": "Title of the OS desktop notification shown when an app update is available." + }, + "notify.update.body": { + "message": "NetBird {version} is available.", + "description": "Body of the update-available desktop notification. {version} is the new version number; keep it." + }, + "notify.update.enforcedSuffix": { + "message": " Your administrator requires this update.", + "description": "Sentence appended to the update notification body when the admin has made the update mandatory. Note the leading space — keep it." + }, + "notify.error.title": { + "message": "Error", + "description": "Title of a generic error desktop notification." + }, + "notify.error.connect": { + "message": "Failed to connect", + "description": "Error notification body shown when connecting failed." + }, + "notify.error.disconnect": { + "message": "Failed to disconnect", + "description": "Error notification body shown when disconnecting failed." + }, + "notify.error.switchProfile": { + "message": "Failed to switch to {profile}", + "description": "Error notification shown when switching profiles failed. {profile} is the target profile name; keep it." + }, + "notify.error.exitNode": { + "message": "Failed to update exit node {name}", + "description": "Error notification shown when updating the exit node failed. {name} is the exit node name; keep it." + }, + "notify.sessionExpired.title": { + "message": "NetBird session expired", + "description": "Title of the desktop notification shown when the session has expired." + }, + "notify.sessionExpired.body": { + "message": "Your NetBird session has expired. Please log in again.", + "description": "Body of the session-expired desktop notification." + }, + "notify.sessionWarning.title": { + "message": "Session expires soon", + "description": "Title of the desktop notification warning that the session will expire soon." + }, + "notify.sessionWarning.body": { + "message": "Your NetBird session expires in {remaining}. Click Extend now to renew.", + "description": "Body of the session-expiry warning notification. {remaining} is a human-readable duration (see tray.session.unit.*); keep it. 'Extend now' refers to the action button notify.sessionWarning.extend." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "Your NetBird session is about to expire. Click Extend now to renew.", + "description": "Generic session-expiry warning body used when the exact remaining time isn't known." + }, + "notify.sessionWarning.extend": { + "message": "Extend now", + "description": "Action button on the session-expiry notification that renews the session. Keep short." + }, + "notify.sessionWarning.dismiss": { + "message": "Dismiss", + "description": "Action button on the session-expiry notification that dismisses it. Keep short." + }, + "notify.sessionWarning.failed": { + "message": "Failed to extend NetBird session", + "description": "Notification shown when renewing the session failed." + }, + "notify.sessionWarning.successTitle": { + "message": "NetBird session extended", + "description": "Title of the notification confirming the session was renewed." + }, + "notify.sessionWarning.successBody": { + "message": "Your session has been refreshed.", + "description": "Body of the notification confirming the session was renewed." + }, + "notify.sessionDeadlineRejected.title": { + "message": "Session deadline rejected", + "description": "Title of the notification shown when the server sent an invalid session deadline." + }, + "notify.sessionDeadlineRejected.body": { + "message": "The server sent an invalid session deadline. Please sign in again.", + "description": "Body explaining the server sent an invalid session deadline and the user must sign in again." + }, + "notify.mdm.policyApplied.title": { + "message": "NetBird settings updated", + "description": "Title of the desktop notification shown when an MDM (IT-managed) policy changed the daemon configuration at runtime." + }, + "notify.mdm.policyApplied.body": { + "message": "Your NetBird configuration was updated by your IT policy.", + "description": "Body of the MDM policy-applied notification, telling the user their settings were changed by their organization's device-management policy." + }, + "common.cancel": { + "message": "Cancel", + "description": "Generic Cancel button label, reused across dialogs. Keep short." + }, + "common.save": { + "message": "Save", + "description": "Generic Save button label. Keep short." + }, + "common.saveChanges": { + "message": "Save Changes", + "description": "Button label to save edited settings. Keep short." + }, + "common.saving": { + "message": "Saving…", + "description": "Button label / status shown while a save is in progress. Ends with an ellipsis." + }, + "common.close": { + "message": "Close", + "description": "Generic Close button label. Keep short." + }, + "common.copy": { + "message": "Copy", + "description": "Generic Copy button label (copy to clipboard). Keep short." + }, + "common.togglePasswordVisibility": { + "message": "Toggle password visibility", + "description": "Accessibility label for the show/hide button inside a password field." + }, + "common.increase": { + "message": "Increase", + "description": "Accessibility label for the increment (+) button on a numeric stepper input." + }, + "common.decrease": { + "message": "Decrease", + "description": "Accessibility label for the decrement (−) button on a numeric stepper input." + }, + "common.delete": { + "message": "Delete", + "description": "Generic Delete button label. Keep short." + }, + "common.create": { + "message": "Create", + "description": "Generic Create button label. Keep short." + }, + "common.add": { + "message": "Add", + "description": "Generic Add button label. Keep short." + }, + "common.remove": { + "message": "Remove", + "description": "Generic Remove button label. Keep short." + }, + "common.refresh": { + "message": "Refresh", + "description": "Generic Refresh button label. Keep short." + }, + "common.loading": { + "message": "Loading…", + "description": "Generic loading indicator text. Ends with an ellipsis." + }, + "common.netbird": { + "message": "NetBird", + "description": "The product name. Brand — do not translate." + }, + "common.noResults.title": { + "message": "Could not find any results", + "description": "Title of the empty state shown when a search or filter returns nothing." + }, + "common.noResults.description": { + "message": "We couldn't find any results. Please try a different search term or change your filters.", + "description": "Body of the no-results empty state, suggesting a different search term or filters." + }, + "notConnected.title": { + "message": "Disconnected", + "description": "Title of the placeholder shown on data screens while disconnected." + }, + "notConnected.description": { + "message": "Connect to NetBird first to view detailed information about your peers, network resources, and exit nodes.", + "description": "Body explaining the user must connect to NetBird first to see peer, resource, and exit-node details." + }, + "connect.status.disconnected": { + "message": "Disconnected", + "description": "Label on the main-window connection toggle: not connected." + }, + "connect.status.connecting": { + "message": "Connecting...", + "description": "Connection toggle label while connecting. Ends with an ellipsis." + }, + "connect.status.connected": { + "message": "Connected", + "description": "Connection toggle label when connected." + }, + "connect.status.disconnecting": { + "message": "Disconnecting...", + "description": "Connection toggle label while disconnecting. Ends with an ellipsis." + }, + "connect.status.daemonUnavailable": { + "message": "Daemon unavailable", + "description": "Connection toggle label when the background service is unavailable. 'Daemon' = background service." + }, + "connect.status.loginRequired": { + "message": "Login required", + "description": "Connection toggle label when sign-in is required before connecting." + }, + "connect.error.loginTitle": { + "message": "Login Failed", + "description": "Error-dialog title shown when sign-in fails. The action-named '… Failed' style is intentional — keep it." + }, + "connect.error.connectTitle": { + "message": "Connect Failed", + "description": "Error-dialog title shown when connecting fails." + }, + "connect.error.disconnectTitle": { + "message": "Disconnect Failed", + "description": "Error-dialog title shown when disconnecting fails." + }, + "nav.peers.title": { + "message": "Peers", + "description": "Navigation label for the Peers section (other devices in the network)." + }, + "nav.peers.description": { + "message": "{connected} of {total} connected", + "description": "Sub-label under Peers showing how many are connected. {connected} and {total} are numbers; keep both." + }, + "nav.resources.title": { + "message": "Resources", + "description": "Navigation label for the Resources section (routed networks)." + }, + "nav.resources.description": { + "message": "{active} of {total} active", + "description": "Sub-label under Resources showing how many are active. {active} and {total} are numbers; keep both." + }, + "nav.exitNode.title": { + "message": "Exit Nodes", + "description": "Navigation label for the Exit Nodes section." + }, + "nav.exitNode.none": { + "message": "Not active", + "description": "Sub-label under Exit Nodes when no exit node is in use." + }, + "nav.exitNode.using": { + "message": "Via {name}", + "description": "Sub-label under Exit Nodes when one is active. {name} is the exit node's name; keep it." + }, + "header.openSettings": { + "message": "Open settings", + "description": "Accessibility label / tooltip for the gear icon that opens Settings." + }, + "header.togglePanel": { + "message": "Toggle side panel", + "description": "Accessibility label / tooltip for the button that shows or hides the side panel." + }, + "profile.selector.loading": { + "message": "Loading...", + "description": "Shown in the profile picker while profiles load. Ends with an ellipsis." + }, + "profile.selector.noProfile": { + "message": "No profile", + "description": "Shown in the profile picker when no profile is selected." + }, + "profile.selector.searchPlaceholder": { + "message": "Search profile by name...", + "description": "Placeholder text in the profile picker's search field." + }, + "profile.selector.emptyTitle": { + "message": "No Profiles Found", + "description": "Title shown in the profile picker when a search matches no profiles." + }, + "profile.selector.emptyDescription": { + "message": "Try a different search term or create a new profile.", + "description": "Body shown when no profiles match the search, suggesting a different term or creating one." + }, + "profile.selector.newProfile": { + "message": "New Profile", + "description": "Button in the profile picker to create a new profile. Keep short." + }, + "profile.selector.moreOptions": { + "message": "More options", + "description": "Accessibility label for the per-profile kebab (⋯) menu." + }, + "profile.selector.deregister": { + "message": "Deregister", + "description": "Per-profile menu action: deregister (sign out of the profile but keep it)." + }, + "profile.selector.delete": { + "message": "Delete", + "description": "Per-profile menu action: delete the profile." + }, + "profile.selector.switchTo": { + "message": "Switch to this profile", + "description": "Tooltip / label for the action that switches to a profile." + }, + "profile.selector.edit": { + "message": "Edit", + "description": "Per-profile menu action: open the edit dialog to rename or change the management server." + }, + "profile.edit.title": { + "message": "Edit Profile", + "description": "Title of the dialog for editing an existing profile." + }, + "profile.edit.submit": { + "message": "Save Changes", + "description": "Submit button on the edit-profile dialog. Keep short." + }, + "profile.dialog.title": { + "message": "Enter Profile Name", + "description": "Title of the dialog for naming a new profile." + }, + "profile.dialog.nameLabel": { + "message": "Profile Name", + "description": "Field label for the profile name." + }, + "profile.dialog.description": { + "message": "Set an easily identifiable name for your profile.", + "description": "Helper text under the profile-name field." + }, + "profile.dialog.placeholder": { + "message": "e.g. Work", + "description": "Example placeholder shown in the profile-name field. 'Work' is a sample value; translate it to a natural example." + }, + "profile.dialog.submit": { + "message": "Add Profile", + "description": "Submit button on the add-profile dialog. Keep short." + }, + "profile.dialog.required": { + "message": "Please enter a profile name, e.g. work, home", + "description": "Validation message shown when the profile name is empty. The examples (work, home) may be localized." + }, + "profile.dialog.managementHelp": { + "message": "Use NetBird Cloud or your own server.", + "description": "Helper text noting the profile can use NetBird Cloud or a self-hosted server." + }, + "profile.dialog.urlUnreachable": { + "message": "Couldn't reach this server. Check the URL, or add the profile anyway if you're sure it's correct.", + "description": "Soft warning when the entered server URL couldn't be reached; the user may add the profile anyway." + }, + "header.menu.settings": { + "message": "Settings...", + "description": "'More' menu item in the header that opens Settings. The trailing '...' signals a window opens." + }, + "header.menu.defaultView": { + "message": "Default View", + "description": "'More' menu item that switches the main window to the compact default view." + }, + "header.menu.advancedView": { + "message": "Advanced View", + "description": "'More' menu item that switches the main window to the wider advanced view." + }, + "header.menu.updateAvailable": { + "message": "Update Available", + "description": "'More' menu item / badge shown when an update is available." + }, + "header.menu.open": { + "message": "Open menu", + "description": "Accessibility label for the header's more (⋮) button that opens the menu." + }, + "header.profile.switch": { + "message": "Switch profile", + "description": "Accessibility label for the header's profile selector button." + }, + "connect.toggle.label": { + "message": "Toggle NetBird connection", + "description": "Accessibility label for the large connect/disconnect toggle on the main page." + }, + "connect.localIp.label": { + "message": "Local IP addresses", + "description": "Accessibility label for the local IP selector that toggles between IPv4 and IPv6 on the main page." + }, + "common.search": { + "message": "Search", + "description": "Accessibility label for a generic search input." + }, + "common.filter": { + "message": "Filter", + "description": "Accessibility label for a generic filter control." + }, + "exitNodes.dropdown.trigger": { + "message": "Select exit node", + "description": "Accessibility label for the exit-node picker button at the bottom of the main page." + }, + "peers.row.label": { + "message": "Open details for {name}, {status}", + "description": "Accessibility label for a peer row in the list. {name} is the peer name and {status} is the connection status; keep both placeholders." + }, + "peers.dialog.title": { + "message": "Peer details", + "description": "Accessibility title (announced to screen readers) for the peer details dialog/panel." + }, + "networks.row.toggle": { + "message": "Toggle {name}", + "description": "Accessibility label for the row-wide toggle on a network/resource. {name} is the resource id; keep the placeholder." + }, + "networks.bulk.label": { + "message": "Toggle all visible resources", + "description": "Accessibility label for the bulk enable/disable button at the bottom of the resources list." + }, + "profile.switch.title": { + "message": "Switch Profile to \"{name}\"?", + "description": "Confirmation-dialog title for switching profiles. {name} is the target profile name, shown in quotes; keep {name} and the surrounding quotes." + }, + "profile.switch.message": { + "message": "Are you sure you want to switch profiles?\nYour current profile will be disconnected.", + "description": "Confirmation body for switching profiles. Contains a line break (\\n) — keep it." + }, + "profile.switch.confirm": { + "message": "Confirm", + "description": "Confirm button on the switch-profile dialog. Keep short." + }, + "profile.deregister.title": { + "message": "Deregister Profile \"{name}\"?", + "description": "Confirmation-dialog title for deregistering a profile. {name} is the profile name; keep it and the quotes." + }, + "profile.deregister.message": { + "message": "Are you sure you want to deregister this profile?\nYou will need to log in again to use it.", + "description": "Confirmation body for deregistering; warns the user must sign in again. Contains a line break (\\n) — keep it." + }, + "profile.deregister.confirm": { + "message": "Deregister", + "description": "Confirm button on the deregister dialog. Keep short." + }, + "profile.delete.title": { + "message": "Delete Profile \"{name}\"?", + "description": "Confirmation-dialog title for deleting a profile. {name} is the profile name; keep it and the quotes." + }, + "profile.delete.message": { + "message": "Are you sure you want to delete this profile?\nThis action cannot be undone.", + "description": "Confirmation body for deleting; warns the action can't be undone. Contains a line break (\\n) — keep it." + }, + "profile.delete.disabledActive": { + "message": "Active profiles cannot be deleted. Switch to a different one before deleting this profile.", + "description": "Tooltip explaining the active profile can't be deleted until the user switches away." + }, + "profile.delete.disabledDefault": { + "message": "The default profile cannot be deleted.", + "description": "Tooltip explaining the default profile can't be deleted." + }, + "profile.error.switchTitle": { + "message": "Switch Profile Failed", + "description": "Error-dialog title when switching profiles fails." + }, + "profile.error.deregisterTitle": { + "message": "Deregister Profile Failed", + "description": "Error-dialog title when deregistering a profile fails." + }, + "profile.error.deleteTitle": { + "message": "Delete Profile Failed", + "description": "Error-dialog title when deleting a profile fails." + }, + "profile.error.createTitle": { + "message": "Create Profile Failed", + "description": "Error-dialog title when creating a profile fails." + }, + "profile.error.editTitle": { + "message": "Edit Profile Failed", + "description": "Error-dialog title when editing a profile (rename or management URL change) fails." + }, + "profile.error.loadTitle": { + "message": "Load Profiles Failed", + "description": "Error-dialog title when loading profiles fails." + }, + "profile.dropdown.activeProfile": { + "message": "Active profile", + "description": "Section heading in the header profile dropdown for the current profile." + }, + "profile.dropdown.switchProfile": { + "message": "Switch Profile", + "description": "Section heading / action in the profile dropdown to switch profiles." + }, + "profile.dropdown.noEmail": { + "message": "Other", + "description": "Label shown for a profile that has no associated email address." + }, + "profile.dropdown.addProfile": { + "message": "Add Profile", + "description": "Profile dropdown action to add a profile." + }, + "profile.dropdown.manageProfiles": { + "message": "Manage Profiles", + "description": "Profile dropdown action that opens Settings → Profiles." + }, + "profile.dropdown.settings": { + "message": "Settings", + "description": "Profile dropdown action that opens Settings." + }, + "settings.profiles.section.profiles": { + "message": "Profiles", + "description": "Section heading on the Profiles settings tab." + }, + "settings.profiles.intro": { + "message": "Keep separate NetBird identities side by side, for example work and personal accounts, or different management servers. Add, deregister, or delete profiles below.", + "description": "Intro paragraph on the Profiles settings tab explaining what profiles are for." + }, + "settings.profiles.addProfile": { + "message": "Add Profile", + "description": "Button on the Profiles settings tab to add a profile." + }, + "settings.profiles.active": { + "message": "Active", + "description": "Badge marking the currently active profile in the profiles table." + }, + "settings.profiles.emptyTitle": { + "message": "No Profiles", + "description": "Title of the empty state when there are no profiles." + }, + "settings.profiles.emptyDescription": { + "message": "Create a profile to connect to a NetBird management server.", + "description": "Body of the no-profiles empty state." + }, + "settings.error.loadTitle": { + "message": "Load Settings Failed", + "description": "Error-dialog title when loading settings fails." + }, + "settings.error.saveTitle": { + "message": "Save Settings Failed", + "description": "Error-dialog title when saving settings fails." + }, + "settings.error.debugBundleTitle": { + "message": "Debug Bundle Failed", + "description": "Error-dialog title when creating the debug bundle fails." + }, + "settings.nav.label": { + "message": "Settings sections", + "description": "Accessibility label for the Settings page's side navigation (list of section tabs)." + }, + "settings.tabs.general": { + "message": "General", + "description": "Settings tab label: General. Keep short." + }, + "settings.tabs.network": { + "message": "Network", + "description": "Settings tab label: Network. Keep short." + }, + "settings.tabs.security": { + "message": "Security", + "description": "Settings tab label: Security. Keep short." + }, + "settings.tabs.profiles": { + "message": "Profiles", + "description": "Settings tab label: Profiles. Keep short." + }, + "settings.tabs.ssh": { + "message": "SSH", + "description": "Settings tab label: SSH. Acronym — keep as-is." + }, + "settings.tabs.advanced": { + "message": "Advanced", + "description": "Settings tab label: Advanced. Keep short." + }, + "settings.tabs.troubleshooting": { + "message": "Troubleshoot", + "description": "Settings tab label: Troubleshoot. Keep short." + }, + "settings.tabs.about": { + "message": "About", + "description": "Settings tab label: About. Keep short." + }, + "settings.tabs.updateAvailable": { + "message": "Update Available", + "description": "Settings tab label / badge shown when an update is available." + }, + "settings.general.section.general": { + "message": "General", + "description": "Section heading on the General settings tab." + }, + "settings.general.section.connection": { + "message": "Connection", + "description": "Section heading for connection-related options on the General tab." + }, + "settings.general.connectOnStartup.label": { + "message": "Connect on Startup", + "description": "Toggle label: connect automatically when the service starts." + }, + "settings.general.connectOnStartup.help": { + "message": "Automatically establish a connection when the service starts.", + "description": "Helper text for the connect-on-startup toggle." + }, + "settings.general.notifications.label": { + "message": "Desktop Notifications", + "description": "Toggle label: enable desktop notifications." + }, + "settings.general.notifications.help": { + "message": "Show desktop notifications for new updates and connection events.", + "description": "Helper text for the desktop-notifications toggle." + }, + "settings.general.autostart.label": { + "message": "Launch NetBird UI at Login", + "description": "Toggle label: launch the NetBird UI at login." + }, + "settings.general.autostart.help": { + "message": "Start the NetBird interface automatically when you log in. This affects the graphical interface only, not the background service.", + "description": "Helper text clarifying autostart affects only the UI, not the background service." + }, + "settings.general.autostart.errorTitle": { + "message": "Autostart Change Failed", + "description": "Error-dialog title when changing the autostart setting fails." + }, + "settings.general.language.label": { + "message": "Display Language", + "description": "Label for the display-language picker." + }, + "settings.general.language.help": { + "message": "Choose the language for the NetBird interface.", + "description": "Helper text for the language picker." + }, + "settings.general.language.search": { + "message": "Search language…", + "description": "Placeholder in the language picker's search field." + }, + "settings.general.language.empty": { + "message": "No languages match.", + "description": "Shown when no languages match the search." + }, + "settings.general.management.label": { + "message": "Management Server", + "description": "Label for the management-server selector." + }, + "settings.general.management.help": { + "message": "Connect to NetBird Cloud or your own self-hosted management server. Changes will reconnect the client.", + "description": "Helper text explaining Cloud vs self-hosted management server; warns that changes reconnect the client." + }, + "settings.general.management.cloud": { + "message": "Cloud", + "description": "Option label for using NetBird Cloud as the management server." + }, + "settings.general.management.selfHosted": { + "message": "Self-hosted", + "description": "Option label for using a self-hosted management server. 'Self-hosted' is a common technical term." + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443", + "description": "Example URL placeholder for the self-hosted server field. It is a sample URL — do not translate the URL itself." + }, + "settings.general.management.urlError": { + "message": "Please enter a valid URL, e.g., https://netbird.selfhosted.com:443", + "description": "Validation message for an invalid management-server URL. The example URL stays as-is." + }, + "settings.general.management.urlUnreachable": { + "message": "Couldn't reach this server. Check the URL, or save anyway if you're sure it's correct.", + "description": "Soft warning when the management URL couldn't be reached; the user may save anyway." + }, + "settings.general.management.switchCloudTitle": { + "message": "Switch to NetBird Cloud?", + "description": "Confirmation-dialog title for switching to NetBird Cloud." + }, + "settings.general.management.switchCloudMessage": { + "message": "This disconnects your self-hosted server.\nYou may need to log in again.", + "description": "Confirmation body warning the self-hosted server will be disconnected. Contains a line break (\\n) — keep it." + }, + "settings.general.management.switchCloudConfirm": { + "message": "Switch to Cloud", + "description": "Confirm button for switching to Cloud. Keep short." + }, + "settings.network.section.connectivity": { + "message": "Connectivity", + "description": "Section heading for connectivity options on the Network tab." + }, + "settings.network.section.routingDns": { + "message": "Routing & DNS", + "description": "Section heading for routing and DNS options. 'DNS' is an acronym — keep it." + }, + "settings.network.lazy.label": { + "message": "Lazy Connections", + "description": "Toggle label: Lazy Connections (on-demand connections)." + }, + "settings.network.lazy.help": { + "message": "Instead of maintaining always-on connections, NetBird activates them on-demand based on activity or signaling.", + "description": "Helper text for lazy connections." + }, + "settings.network.monitor.label": { + "message": "Reconnect on Network Change", + "description": "Toggle label: reconnect automatically on network change." + }, + "settings.network.monitor.help": { + "message": "Monitor the network and automatically reconnect on changes such as Wi-Fi switching, Ethernet changes, or resume from sleep.", + "description": "Helper text for the network-change reconnect toggle." + }, + "settings.network.dns.label": { + "message": "Enable DNS", + "description": "Toggle label: enable DNS. 'DNS' — keep acronym." + }, + "settings.network.dns.help": { + "message": "Apply NetBird-managed DNS settings to the host resolver.", + "description": "Helper text for the enable-DNS toggle." + }, + "settings.network.clientRoutes.label": { + "message": "Enable Client Routes", + "description": "Toggle label: enable client routes." + }, + "settings.network.clientRoutes.help": { + "message": "Accept routes from other peers to reach their networks.", + "description": "Helper text for client routes (accept routes from other peers)." + }, + "settings.network.serverRoutes.label": { + "message": "Enable Server Routes", + "description": "Toggle label: enable server routes." + }, + "settings.network.serverRoutes.help": { + "message": "Advertise this host's local routes to other peers.", + "description": "Helper text for server routes (advertise this host's local routes to other peers)." + }, + "settings.network.ipv6.label": { + "message": "Enable IPv6", + "description": "Toggle label: enable IPv6. 'IPv6' — keep as-is." + }, + "settings.network.ipv6.help": { + "message": "Use IPv6 addressing for the NetBird overlay network.", + "description": "Helper text for the IPv6 toggle." + }, + "settings.security.section.firewall": { + "message": "Firewall", + "description": "Section heading: Firewall." + }, + "settings.security.section.encryption": { + "message": "Encryption", + "description": "Section heading: Encryption." + }, + "settings.security.blockInbound.label": { + "message": "Block Inbound Traffic", + "description": "Toggle label: block inbound traffic." + }, + "settings.security.blockInbound.help": { + "message": "Reject unsolicited connections from peers to this device and any networks it routes. Outbound traffic is unaffected.", + "description": "Helper text for blocking inbound traffic." + }, + "settings.security.blockLan.label": { + "message": "Block LAN Access", + "description": "Toggle label: block LAN access. 'LAN' — keep acronym." + }, + "settings.security.blockLan.help": { + "message": "Prevent peers from reaching your local network or its devices when this device routes their traffic.", + "description": "Helper text for blocking LAN access." + }, + "settings.security.rosenpass.label": { + "message": "Enable Quantum-Resistance", + "description": "Toggle label: enable quantum-resistance." + }, + "settings.security.rosenpass.help": { + "message": "Add a post-quantum key exchange via Rosenpass on top of WireGuard®.", + "description": "Helper text: adds a post-quantum key exchange via Rosenpass on top of WireGuard®. 'Rosenpass' and 'WireGuard®' are product names — do not translate; keep the ® symbol." + }, + "settings.security.rosenpassPermissive.label": { + "message": "Enable Permissive Mode", + "description": "Toggle label: enable permissive mode (for quantum-resistance)." + }, + "settings.security.rosenpassPermissive.help": { + "message": "Allow connections to peers without quantum-resistance support.", + "description": "Helper text for permissive mode (allow peers without quantum-resistance support)." + }, + "settings.ssh.section.server": { + "message": "Server", + "description": "Section heading: Server (SSH settings)." + }, + "settings.ssh.section.capabilities": { + "message": "Capabilities", + "description": "Section heading: Capabilities (SSH features)." + }, + "settings.ssh.section.authentication": { + "message": "Authentication", + "description": "Section heading: Authentication (SSH)." + }, + "settings.ssh.server.label": { + "message": "Enable SSH Server", + "description": "Toggle label: enable the SSH server." + }, + "settings.ssh.server.help": { + "message": "Run the NetBird SSH server on this host so other peers can connect to it.", + "description": "Helper text for the SSH server toggle." + }, + "settings.ssh.root.label": { + "message": "Allow Root Login", + "description": "Toggle label: allow root login over SSH. 'root' is the Unix superuser account — keep as-is." + }, + "settings.ssh.root.help": { + "message": "Let peers sign in as the root user. Disable to require a non-privileged account.", + "description": "Helper text for allowing root login." + }, + "settings.ssh.sftp.label": { + "message": "Allow SFTP", + "description": "Toggle label: allow SFTP. 'SFTP' — keep acronym." + }, + "settings.ssh.sftp.help": { + "message": "Transfer files securely using native SFTP or SCP clients.", + "description": "Helper text about SFTP/SCP file transfer. Keep the acronyms." + }, + "settings.ssh.localForward.label": { + "message": "Local Port Forwarding", + "description": "Toggle label: local port forwarding." + }, + "settings.ssh.localForward.help": { + "message": "Let connecting peers tunnel local ports to services reachable from this host.", + "description": "Helper text for local port forwarding." + }, + "settings.ssh.remoteForward.label": { + "message": "Remote Port Forwarding", + "description": "Toggle label: remote port forwarding." + }, + "settings.ssh.remoteForward.help": { + "message": "Let connecting peers expose ports on this host back to their own machine.", + "description": "Helper text for remote port forwarding." + }, + "settings.ssh.jwt.label": { + "message": "Enable JWT Authentication", + "description": "Toggle label: enable JWT authentication. 'JWT' — keep acronym." + }, + "settings.ssh.jwt.help": { + "message": "Verify each SSH session against your IdP for user identity and audit. Disable to rely on network ACL policies only, useful when no IdP is available.", + "description": "Helper text for JWT auth. 'IdP' (identity provider) and 'ACL' are acronyms — keep them." + }, + "settings.ssh.jwtTtl.label": { + "message": "JWT Cache TTL", + "description": "Label for the JWT cache time-to-live field. 'JWT' and 'TTL' — keep acronyms." + }, + "settings.ssh.jwtTtl.help": { + "message": "How long this client caches a JWT before prompting again on outgoing SSH connections. Set to 0 to disable caching and authenticate on every connection.", + "description": "Helper text for the JWT cache TTL; mentions setting 0 to disable caching." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "Second(s)", + "description": "Unit suffix shown after the JWT TTL number field. The '(s)' marks an optional plural." + }, + "settings.advanced.section.interface": { + "message": "Interface", + "description": "Section heading: Interface (network-interface settings)." + }, + "settings.advanced.section.security": { + "message": "Security", + "description": "Section heading: Security (advanced)." + }, + "settings.advanced.interfaceName.label": { + "message": "Name", + "description": "Field label for the WireGuard interface name." + }, + "settings.advanced.interfaceName.error": { + "message": "Use 1-15 letters, digits, dots, hyphens, or underscores.", + "description": "Validation message for the interface name (allowed characters)." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "Must start with \"utun\" followed by a number (e.g. utun100).", + "description": "Validation message specific to macOS, where the name must start with 'utun' followed by a number. Keep 'utun' and the example." + }, + "settings.advanced.port.label": { + "message": "Port", + "description": "Field label: Port." + }, + "settings.advanced.port.error": { + "message": "Enter a port between {min} and {max}.", + "description": "Validation message for the port range. {min} and {max} are numbers; keep them." + }, + "settings.advanced.port.help": { + "message": "If set to 0, a random free port will be used.", + "description": "Helper text: 0 means a random free port is used." + }, + "settings.advanced.mtu.label": { + "message": "MTU", + "description": "Field label: MTU. 'MTU' — keep acronym." + }, + "settings.advanced.mtu.error": { + "message": "Enter an MTU value between {min} and {max}.", + "description": "Validation message for the MTU range. {min} and {max} are numbers; keep them." + }, + "settings.advanced.psk.label": { + "message": "Pre-shared Key", + "description": "Field label: Pre-shared Key." + }, + "settings.advanced.psk.help": { + "message": "Optional WireGuard PSK for extra symmetric encryption. Not the same as a NetBird Setup Key. You will only communicate with peers that use the same pre-shared key.", + "description": "Helper text for the WireGuard PSK. 'WireGuard', 'PSK', and 'NetBird Setup Key' are product/technical terms — keep them." + }, + "settings.troubleshooting.section.title": { + "message": "Debug bundle", + "description": "Section heading: Debug bundle." + }, + "settings.troubleshooting.anonymize.label": { + "message": "Anonymize Sensitive Information", + "description": "Toggle label: anonymize sensitive information in the bundle." + }, + "settings.troubleshooting.anonymize.help": { + "message": "Hides public IP addresses and non-NetBird domains from logs.", + "description": "Helper text for anonymizing logs (hides public IPs and non-NetBird domains)." + }, + "settings.troubleshooting.systemInfo.label": { + "message": "Include System Information", + "description": "Toggle label: include system information in the bundle." + }, + "settings.troubleshooting.systemInfo.help": { + "message": "Include OS, kernel, network interfaces, and routing tables.", + "description": "Helper text listing the system info included (OS, kernel, interfaces, routing tables)." + }, + "settings.troubleshooting.upload.label": { + "message": "Upload Bundle to NetBird Servers", + "description": "Toggle label: upload the bundle to NetBird servers." + }, + "settings.troubleshooting.upload.help": { + "message": "Returns an upload key to share with NetBird support.", + "description": "Helper text for uploading the bundle." + }, + "settings.troubleshooting.trace.label": { + "message": "Enable Trace Logs", + "description": "Toggle label: raise daemon log level to TRACE while the bundle is built. 'TRACE' is a log level." + }, + "settings.troubleshooting.trace.help": { + "message": "Raises the log level to TRACE and restores it after.", + "description": "Helper text for the trace toggle. 'TRACE' is a log level — keep as-is." + }, + "settings.troubleshooting.capture.label": { + "message": "Capture Session", + "description": "Toggle label: open a capture session — reconnect NetBird, wait a duration, optionally record packets." + }, + "settings.troubleshooting.capture.help": { + "message": "Reconnects and waits so you can reproduce the issue.", + "description": "Helper text for the master Capture Session toggle." + }, + "settings.troubleshooting.packets.label": { + "message": "Capture Network Packets", + "description": "Toggle label: capture packets to a .pcap during the capture session." + }, + "settings.troubleshooting.packets.help": { + "message": "Saves a .pcap of network traffic during the capture window.", + "description": "Helper text for the packet recording toggle. '.pcap' is a file extension — keep it." + }, + "settings.troubleshooting.duration.label": { + "message": "Capture Duration", + "description": "Label for the trace-capture duration field." + }, + "settings.troubleshooting.duration.help": { + "message": "How long the capture session runs.", + "description": "Helper text for the capture duration." + }, + "settings.troubleshooting.duration.suffix": { + "message": "Minute(s)", + "description": "Unit suffix after the duration field. The '(s)' marks an optional plural." + }, + "settings.troubleshooting.create": { + "message": "Create Bundle", + "description": "Button to create the debug bundle. Keep short." + }, + "settings.troubleshooting.progress.description": { + "message": "Collecting logs, system details, and connection state. This usually takes a moment. You can keep using NetBird or close Settings while it finishes.", + "description": "Status text shown while the debug bundle is being collected; reassures the user they can keep using NetBird or close Settings meanwhile." + }, + "settings.troubleshooting.cancelling": { + "message": "Canceling…", + "description": "Status shown while cancelling bundle creation. Ends with an ellipsis." + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "Debug bundle successfully uploaded!", + "description": "Success title after the bundle was uploaded." + }, + "settings.troubleshooting.done.savedTitle": { + "message": "Bundle saved", + "description": "Title shown when the bundle was saved locally (not uploaded)." + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "Share the upload key below with NetBird support. A local copy was also saved on your device.", + "description": "Body after upload, telling the user to share the upload key with support. '' wraps the inline link to the NetBird support docs — keep the tags exactly and wrap the phrase that means 'NetBird support'." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "Your debug bundle has been saved locally.", + "description": "Body shown when the bundle was only saved locally." + }, + "settings.troubleshooting.done.copyKey": { + "message": "Copy Key", + "description": "Button to copy the upload key. Keep short." + }, + "settings.troubleshooting.done.openFolder": { + "message": "Open Folder", + "description": "Button to open the folder containing the bundle. Keep short." + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "Open file location", + "description": "Button to reveal the bundle file in the OS file manager." + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "Upload failed: {reason} The bundle is still saved locally.", + "description": "Shown when upload failed but the bundle was saved locally. {reason} is the server error text; keep it." + }, + "settings.troubleshooting.uploadFailed": { + "message": "Upload failed. The bundle is still saved locally.", + "description": "Shown when upload failed (no specific reason) but the bundle was saved locally." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "Reconnecting NetBird…", + "description": "Progress stage: reconnecting NetBird. Ends with an ellipsis." + }, + "settings.troubleshooting.stage.capturing": { + "message": "Capturing debug logs", + "description": "Progress stage: capturing logs. {elapsed} and {total} are time values (e.g. 0:30 / 2:00); keep both." + }, + "settings.troubleshooting.stage.bundling": { + "message": "Generating debug bundle…", + "description": "Progress stage: generating the debug bundle. Ends with an ellipsis." + }, + "settings.troubleshooting.stage.uploading": { + "message": "Uploading to NetBird…", + "description": "Progress stage: uploading to NetBird. Ends with an ellipsis." + }, + "settings.troubleshooting.stage.cancelling": { + "message": "Canceling…", + "description": "Progress stage: cancelling. Ends with an ellipsis." + }, + "settings.about.client": { + "message": "NetBird Client v{version}", + "description": "About tab: client product name and version. {version} is a version number; keep it. 'NetBird Client' — keep brand." + }, + "settings.about.clientName": { + "message": "NetBird Client", + "description": "About tab: the client product name shown when no version is available. Brand — do not translate." + }, + "settings.about.development": { + "message": "[Development]", + "description": "Badge shown next to the version on development builds. Keep the square brackets." + }, + "settings.about.gui": { + "message": "GUI v{version}", + "description": "About tab: UI component name and version. {version} is a version number; keep it. 'GUI' = the graphical app." + }, + "settings.about.guiName": { + "message": "GUI", + "description": "About tab: the UI component name shown without a version. 'GUI' = the graphical app." + }, + "settings.about.copyright": { + "message": "© {year} NetBird. All Rights Reserved.", + "description": "Copyright line. {year} is the current year; keep it. 'NetBird' — brand." + }, + "settings.about.links.imprint": { + "message": "Imprint", + "description": "Footer link: Imprint (legal notice / Impressum)." + }, + "settings.about.links.privacy": { + "message": "Privacy", + "description": "Footer link: Privacy policy." + }, + "settings.about.links.cla": { + "message": "CLA", + "description": "Footer link: CLA (Contributor License Agreement). Acronym — keep as-is." + }, + "settings.about.links.terms": { + "message": "Terms of Service", + "description": "Footer link: Terms of Service." + }, + "settings.about.community.github": { + "message": "GitHub", + "description": "Community link: GitHub. Brand — do not translate." + }, + "settings.about.community.slack": { + "message": "Slack", + "description": "Community link: Slack. Brand — do not translate." + }, + "settings.about.community.forum": { + "message": "Forum", + "description": "Community link: Forum." + }, + "settings.about.community.documentation": { + "message": "Documentation", + "description": "Community link: Documentation." + }, + "settings.about.community.feedback": { + "message": "Feedback", + "description": "Community link: Feedback." + }, + "update.banner.message": { + "message": "NetBird {version} is ready to install.", + "description": "Update banner text when a new version is ready to install. {version} is the version number; keep it." + }, + "update.banner.later": { + "message": "Later", + "description": "Banner button to postpone the update. Keep short." + }, + "update.banner.installNow": { + "message": "Install now", + "description": "Banner button to install the update now. Keep short." + }, + "update.card.versionAvailableDownload": { + "message": "Version {version} is available for download.", + "description": "Update card text when a version is available to download. {version} is the version number; keep it." + }, + "update.card.versionAvailableInstall": { + "message": "Version {version} is available for install.", + "description": "Update card text when a version is available to install. {version} is the version number; keep it." + }, + "update.card.whatsNew": { + "message": "What's new?", + "description": "Link / label that opens the release notes." + }, + "update.card.installNow": { + "message": "Install Now", + "description": "Update card button: install now. Keep short." + }, + "update.card.getInstaller": { + "message": "Download", + "description": "Update card button: download the installer." + }, + "update.card.autoCheckInterval": { + "message": "NetBird checks for updates in the background.", + "description": "Note that NetBird checks for updates in the background." + }, + "update.card.changelog": { + "message": "Changelog", + "description": "Link / label: Changelog." + }, + "update.card.onLatestVersion": { + "message": "You're on the latest version", + "description": "Shown when the client is already up to date." + }, + "update.header.tooltip": { + "message": "Update Available", + "description": "Tooltip on the header update badge." + }, + "update.overlay.updatingVersion": { + "message": "Updating NetBird to v{version}", + "description": "Heading in the install-progress window while updating to a specific version. {version} is the version number; keep it. The 'v' prefix is part of the version display." + }, + "update.overlay.updating": { + "message": "Updating NetBird", + "description": "Heading in the install-progress window while updating when the target version isn't known." + }, + "update.overlay.description": { + "message": "A newer version is available and is being installed. NetBird will restart automatically once the update is finished.", + "description": "Install-progress window body explaining NetBird will restart automatically after the update." + }, + "update.overlay.error.timeoutTitle": { + "message": "Update Is Taking Too Long", + "description": "Install-progress window error title: the update took too long." + }, + "update.overlay.error.timeoutDescription": { + "message": "Installing {target} took too long and didn't finish.", + "description": "Install-progress window error body for a timeout. {target} is the target-version label (see update.overlay.error.targetVersion / targetFallback); keep it." + }, + "update.overlay.error.canceledTitle": { + "message": "Update Was Stopped", + "description": "Install-progress window error title: the update was stopped / canceled." + }, + "update.overlay.error.canceledDescription": { + "message": "The update to {target} was canceled before it finished.", + "description": "Install-progress window error body for a canceled update. {target} is the target-version label; keep it." + }, + "update.overlay.error.failTitle": { + "message": "Couldn't Install the Update", + "description": "Install-progress window error title: the update couldn't be installed." + }, + "update.overlay.error.failDescription": { + "message": "{target} couldn't be installed.", + "description": "Install-progress window error body for a failed install. {target} is the target-version label; keep it." + }, + "update.overlay.error.unknownMessage": { + "message": "unknown error", + "description": "Fallback text for an unspecified error, used inside other messages." + }, + "update.overlay.error.targetVersion": { + "message": "v{version}", + "description": "The target-version label substituted into {target}. {version} is a number; keep the 'v' prefix." + }, + "update.overlay.error.targetFallback": { + "message": "the new version", + "description": "Fallback target label used when the version isn't known, substituted into {target}." + }, + "update.error.loadStateTitle": { + "message": "Load Update State Failed", + "description": "Error-dialog title when loading update state fails." + }, + "update.error.triggerTitle": { + "message": "Start Update Failed", + "description": "Error-dialog title when starting the update fails." + }, + "update.page.versionLine": { + "message": "Updating client to: {version}.", + "description": "Install-progress window text naming the target version. {version} is the version number; keep it." + }, + "update.page.versionLineGeneric": { + "message": "Updating client.", + "description": "Install-progress text used when the target version isn't known." + }, + "update.page.outdated": { + "message": "Your client version is older than the auto-update version set in Management.", + "description": "Note that the client is older than the auto-update version set in Management. 'Management' = the NetBird management server / console." + }, + "update.page.status.running": { + "message": "Updating", + "description": "Install status: updating." + }, + "update.page.status.timeout": { + "message": "Update timed out. Please try again.", + "description": "Install status: timed out; asks the user to retry." + }, + "update.page.status.canceled": { + "message": "Update canceled.", + "description": "Install status: canceled." + }, + "update.page.status.failed": { + "message": "Update failed: {message}", + "description": "Install status: failed. {message} is the error detail; keep it." + }, + "update.page.status.unknownError": { + "message": "unknown update error", + "description": "Fallback text for an unknown update error, used inside update.page.status.failed." + }, + "update.page.failedTitle": { + "message": "Update Failed", + "description": "Heading shown when the install failed." + }, + "update.page.timeoutMessage": { + "message": "Update timed out.", + "description": "Message shown when the install timed out." + }, + "update.page.dontClose": { + "message": "Please don't close this window.", + "description": "Warning asking the user not to close the install window." + }, + "update.page.updating": { + "message": "Updating…", + "description": "Short status: updating. Ends with an ellipsis." + }, + "update.page.complete": { + "message": "Update complete", + "description": "Short status: update complete." + }, + "update.page.failed": { + "message": "Update failed", + "description": "Short status: update failed." + }, + "window.title.settings": { + "message": "Settings", + "description": "OS window-chrome title for the Settings window. The app prefixes it with 'NetBird - '." + }, + "window.title.signIn": { + "message": "Sign-in", + "description": "OS window-chrome title for the sign-in window." + }, + "window.title.sessionExpiration": { + "message": "Session Expiring", + "description": "OS window-chrome title for the session-expiration window." + }, + "window.title.updating": { + "message": "Updating", + "description": "OS window-chrome title for the update / install window." + }, + "window.title.welcome": { + "message": "Welcome to NetBird", + "description": "OS window-chrome title for the first-launch welcome window. 'NetBird' — brand." + }, + "window.title.error": { + "message": "Error", + "description": "OS window-chrome title for the error window." + }, + "welcome.title": { + "message": "Look for NetBird in your tray", + "description": "Heading on the first onboarding step, pointing the user to the tray icon. 'tray' = system tray / menu bar." + }, + "welcome.description": { + "message": "NetBird lives in your tray. Click the icon to connect, switch profiles, or open settings.", + "description": "Body of the first onboarding step explaining the tray icon." + }, + "welcome.continue": { + "message": "Continue", + "description": "Primary button to advance the onboarding. Keep short." + }, + "welcome.back": { + "message": "Back", + "description": "Button to go back a step in onboarding. Keep short." + }, + "welcome.management.title": { + "message": "Set up NetBird", + "description": "Heading on the onboarding step for choosing a management server." + }, + "welcome.management.description": { + "message": "Click Continue to get started, or pick Self-hosted if you have your own NetBird server.", + "description": "Body of the management step; mentions choosing Self-hosted if the user runs their own server." + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud", + "description": "Option title: NetBird Cloud. Brand — do not translate 'NetBird Cloud'." + }, + "welcome.management.cloud.description": { + "message": "Use our hosted service. No setup required.", + "description": "Option body for NetBird Cloud (hosted service, no setup required)." + }, + "welcome.management.selfHosted.title": { + "message": "Self-hosted", + "description": "Option title: Self-hosted." + }, + "welcome.management.selfHosted.description": { + "message": "Connect to your own management server.", + "description": "Option body for connecting to your own management server." + }, + "welcome.management.urlLabel": { + "message": "Management server URL", + "description": "Field label for the management-server URL." + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443", + "description": "Example URL placeholder. Sample URL — do not translate it." + }, + "welcome.management.urlInvalid": { + "message": "Please enter a valid URL, e.g., https://netbird.selfhosted.com:443", + "description": "Validation message for an invalid URL. The example URL stays as-is." + }, + "welcome.management.urlUnreachable": { + "message": "Couldn't reach this server. Check the URL or your network, then continue if you're sure it's correct.", + "description": "Soft warning when the server couldn't be reached during onboarding; the user may continue anyway." + }, + "welcome.management.checking": { + "message": "Checking…", + "description": "Status shown while checking the server URL. Ends with an ellipsis." + }, + "browserLogin.title": { + "message": "Complete login in your browser", + "description": "Heading telling the user to finish signing in via their browser." + }, + "browserLogin.notSeeing": { + "message": "We opened a browser tab so you can finish signing in. Not seeing it?", + "description": "Prompt asking whether the user doesn't see the opened browser tab." + }, + "browserLogin.tryAgain": { + "message": "Try again", + "description": "Button to re-open the browser sign-in page. Keep short." + }, + "browserLogin.openFailedTitle": { + "message": "Open Browser Failed", + "description": "Error-dialog title when the browser couldn't be opened." + }, + "sessionExpiration.title": { + "message": "Session expiring soon", + "description": "Heading warning the session will expire soon." + }, + "sessionExpiration.titleLater": { + "message": "Your session will expire", + "description": "Alternate, less-urgent heading: the session will expire." + }, + "sessionExpiration.description": { + "message": "This device will disconnect soon. Renew with a browser sign-in.", + "description": "Body warning the device will disconnect soon; renew via a browser sign-in." + }, + "sessionExpiration.descriptionLater": { + "message": "A browser sign-in keeps this device connected to your network.", + "description": "Body for the less-urgent variant; a browser sign-in keeps the device connected." + }, + "sessionExpiration.stay": { + "message": "Renew session", + "description": "Button to renew the session (keep connected). Keep short." + }, + "sessionExpiration.authenticate": { + "message": "Authenticate", + "description": "Button to authenticate / sign in. Keep short." + }, + "sessionExpiration.logout": { + "message": "Logout", + "description": "Button to log out. Keep short." + }, + "sessionExpiration.expired": { + "message": "Session expired", + "description": "Heading shown once the session has actually expired." + }, + "sessionExpiration.expiredDescription": { + "message": "Device disconnected. Authenticate with a browser sign-in to reconnect.", + "description": "Body shown after expiry; authenticate via a browser sign-in to reconnect." + }, + "sessionExpiration.close": { + "message": "Close", + "description": "Close button on the session window. Keep short." + }, + "sessionExpiration.extendFailedTitle": { + "message": "Extend Session Failed", + "description": "Error-dialog title when extending the session fails." + }, + "sessionExpiration.logoutFailedTitle": { + "message": "Logout Failed", + "description": "Error-dialog title when logging out fails." + }, + "peers.search.placeholder": { + "message": "Search by name or IP", + "description": "Placeholder in the peers search field." + }, + "peers.filter.all": { + "message": "All", + "description": "Peers filter option: All. Keep short." + }, + "peers.filter.online": { + "message": "Online", + "description": "Peers filter option: Online. Keep short." + }, + "peers.filter.offline": { + "message": "Offline", + "description": "Peers filter option: Offline. Keep short." + }, + "peers.empty.title": { + "message": "No peers available", + "description": "Title of the empty state when no peers are available." + }, + "peers.empty.description": { + "message": "You either don't have any peers available or have no access to any of them.", + "description": "Body of the no-peers empty state." + }, + "peers.details.domain": { + "message": "Domain", + "description": "Peer detail label: Domain." + }, + "peers.details.netbirdIp": { + "message": "NetBird IP", + "description": "Peer detail label: NetBird IP address. 'NetBird' — brand; 'IP' — keep acronym." + }, + "peers.details.netbirdIpv6": { + "message": "NetBird IPv6", + "description": "Peer detail label: NetBird IPv6 address. Keep 'NetBird' and 'IPv6'." + }, + "peers.details.publicKey": { + "message": "Public key", + "description": "Peer detail label: WireGuard public key." + }, + "peers.details.connection": { + "message": "Connection", + "description": "Peer detail label: Connection." + }, + "peers.details.latency": { + "message": "Latency", + "description": "Peer detail label: Latency." + }, + "peers.details.lastHandshake": { + "message": "Last handshake", + "description": "Peer detail label: time of the last WireGuard handshake." + }, + "peers.details.statusSince": { + "message": "Last connection update", + "description": "Peer detail label: time since the last connection-status update." + }, + "peers.details.bytes": { + "message": "Bytes", + "description": "Peer detail label: Bytes (data transferred)." + }, + "peers.details.bytesSent": { + "message": "Sent", + "description": "Peer detail label: bytes sent." + }, + "peers.details.bytesReceived": { + "message": "Received", + "description": "Peer detail label: bytes received." + }, + "peers.details.localIce": { + "message": "Local ICE", + "description": "Peer detail label: local ICE candidate. 'ICE' — keep acronym." + }, + "peers.details.remoteIce": { + "message": "Remote ICE", + "description": "Peer detail label: remote ICE candidate. 'ICE' — keep acronym." + }, + "peers.details.never": { + "message": "Never", + "description": "Value shown when an event has never happened (e.g. last handshake = Never)." + }, + "peers.details.justNow": { + "message": "Just now", + "description": "Relative-time value meaning a moment ago." + }, + "peers.details.refresh": { + "message": "Refresh", + "description": "Button to refresh peer details. Keep short." + }, + "peers.status.connected": { + "message": "Connected", + "description": "Peer status: connected." + }, + "peers.status.connecting": { + "message": "Connecting", + "description": "Peer status: connecting." + }, + "peers.status.disconnected": { + "message": "Disconnected", + "description": "Peer status: disconnected." + }, + "peers.details.relayAddress": { + "message": "Relay", + "description": "Peer detail label: Relay (relay-server address)." + }, + "peers.details.networks": { + "message": "Resources", + "description": "Peer detail label for the peer's network resources. Labelled 'Resources' in the UI." + }, + "peers.details.relayed": { + "message": "Relayed", + "description": "Connection-type value: the connection is relayed (not direct). Technical term." + }, + "peers.details.p2p": { + "message": "P2P", + "description": "Connection-type value: peer-to-peer (direct). 'P2P' — keep as-is." + }, + "peers.details.rosenpass": { + "message": "Rosenpass enabled", + "description": "Peer detail indicating Rosenpass (quantum-resistance) is enabled. 'Rosenpass' — product name, do not translate." + }, + "networks.search.placeholder": { + "message": "Search by network or domain", + "description": "Placeholder in the resources search field." + }, + "networks.filter.all": { + "message": "All", + "description": "Resources filter: All. Keep short." + }, + "networks.filter.active": { + "message": "Active", + "description": "Resources filter: Active. Keep short." + }, + "networks.filter.overlapping": { + "message": "Overlapping", + "description": "Resources filter: Overlapping (overlapping IP ranges). Keep short." + }, + "networks.empty.title": { + "message": "No resources available", + "description": "Title of the empty state when no resources are available." + }, + "networks.empty.description": { + "message": "You either don't have any network resources available or have no access to any of them.", + "description": "Body of the no-resources empty state." + }, + "networks.selected": { + "message": "Selected", + "description": "Label / badge: the resource is selected (enabled)." + }, + "networks.unselected": { + "message": "Not selected", + "description": "Label / badge: the resource is not selected." + }, + "networks.ips.heading": { + "message": "Resolved IPs", + "description": "Heading for the list of a resource's resolved IP addresses." + }, + "networks.bulk.selectionCount": { + "message": "{selected} of {total} Active", + "description": "Header showing how many resources are active. {selected} and {total} are numbers; keep both." + }, + "networks.bulk.enableAll": { + "message": "Enable all", + "description": "Button to enable all resources. Keep short." + }, + "networks.bulk.disableAll": { + "message": "Disable all", + "description": "Button to disable all resources. Keep short." + }, + "exitNodes.search.placeholder": { + "message": "Search exit nodes", + "description": "Placeholder in the exit-nodes search field." + }, + "exitNodes.none": { + "message": "None", + "description": "Option meaning no exit node. Keep short." + }, + "exitNodes.empty.title": { + "message": "No exit nodes available", + "description": "Title of the empty state when no exit nodes are available." + }, + "exitNodes.empty.description": { + "message": "No exit nodes have been shared with this peer.", + "description": "Body of the no-exit-nodes empty state." + }, + "exitNodes.card.title": { + "message": "Exit Node", + "description": "Card heading: Exit Node." + }, + "exitNodes.card.statusActive": { + "message": "Active", + "description": "Exit node card status: Active." + }, + "exitNodes.card.statusInactive": { + "message": "Inactive", + "description": "Exit node card status: Inactive." + }, + "exitNodes.dropdown.noneTitle": { + "message": "None", + "description": "Dropdown option title: None (no exit node)." + }, + "exitNodes.dropdown.noneDescription": { + "message": "Direct connection without an exit node", + "description": "Dropdown option body for None: direct connection without an exit node." + }, + "quickActions.connect": { + "message": "Connect", + "description": "Quick-action button: Connect. Keep short." + }, + "quickActions.disconnect": { + "message": "Disconnect", + "description": "Quick-action button: Disconnect. Keep short." + }, + "daemon.unavailable.title": { + "message": "NetBird Service Is Not Running", + "description": "Title of the overlay shown when the NetBird background service isn't running." + }, + "daemon.unavailable.description": { + "message": "The app will reconnect automatically once the service is running.", + "description": "Body reassuring the user the app will reconnect once the service is running." + }, + "daemon.unavailable.docsLink": { + "message": "Documentation", + "description": "Documentation link on the daemon-unavailable overlay." + }, + "daemon.outdated.title": { + "message": "NetBird Service Is Outdated", + "description": "Title of the overlay shown when the NetBird background service is too old to drive this UI." + }, + "daemon.outdated.description": { + "message": "Update the NetBird service to use this app.", + "description": "Body of the daemon-outdated overlay telling the user to upgrade the service." + }, + "error.jwt_clock_skew": { + "message": "Sign-in failed: this device's clock is out of sync with the server. Please sync your system clock and try again.", + "description": "Sign-in error shown to the user: the device clock is out of sync with the server." + }, + "error.jwt_expired": { + "message": "Your sign-in token has expired. Please sign in again.", + "description": "Sign-in error: the sign-in token expired; sign in again." + }, + "error.jwt_signature_invalid": { + "message": "Sign-in failed: the token signature is invalid. Please contact your administrator.", + "description": "Sign-in error: the token signature is invalid; contact the administrator." + }, + "error.session_expired": { + "message": "Your session has expired. Please sign in again.", + "description": "Error: the session expired; sign in again." + }, + "error.invalid_setup_key": { + "message": "The setup key is missing or invalid.", + "description": "Error: the setup key is missing or invalid. 'setup key' is a NetBird term." + }, + "error.permission_denied": { + "message": "Sign-in was rejected by the server.", + "description": "Error: the server rejected the sign-in." + }, + "error.daemon_unreachable": { + "message": "The NetBird daemon is not responding. Please check that the service is running.", + "description": "Error: the NetBird background service isn't responding. 'daemon' = the background service." + }, + "error.unknown": { + "message": "Operation failed.", + "description": "Generic fallback error message used when no specific error applies." + } +} diff --git a/client/ui/i18n/locales/es/common.json b/client/ui/i18n/locales/es/common.json new file mode 100644 index 000000000..6276dd7f5 --- /dev/null +++ b/client/ui/i18n/locales/es/common.json @@ -0,0 +1,1331 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "Desconectado" + }, + "tray.status.daemonUnavailable": { + "message": "No está en ejecución" + }, + "tray.status.error": { + "message": "Error" + }, + "tray.status.connected": { + "message": "Conectado" + }, + "tray.status.connecting": { + "message": "Conectando" + }, + "tray.status.needsLogin": { + "message": "Inicio de sesión requerido" + }, + "tray.status.loginFailed": { + "message": "Error al iniciar sesión" + }, + "tray.status.sessionExpired": { + "message": "Sesión expirada" + }, + "tray.session.expiresIn": { + "message": "La sesión expira en {remaining}" + }, + "tray.session.unit.lessThanMinute": { + "message": "menos de un minuto" + }, + "tray.session.unit.minute": { + "message": "1 minuto" + }, + "tray.session.unit.minutes": { + "message": "{count} minutos" + }, + "tray.session.unit.hour": { + "message": "1 hora" + }, + "tray.session.unit.hours": { + "message": "{count} horas" + }, + "tray.session.unit.day": { + "message": "1 día" + }, + "tray.session.unit.days": { + "message": "{count} días" + }, + "tray.menu.open": { + "message": "Abrir NetBird" + }, + "tray.menu.connect": { + "message": "Conectar" + }, + "tray.menu.disconnect": { + "message": "Desconectar" + }, + "tray.menu.exitNode": { + "message": "Nodo de salida" + }, + "tray.menu.networks": { + "message": "Recursos" + }, + "tray.menu.profiles": { + "message": "Perfiles" + }, + "tray.menu.manageProfiles": { + "message": "Gestionar perfiles" + }, + "tray.menu.settings": { + "message": "Configuración..." + }, + "tray.menu.debugBundle": { + "message": "Crear paquete de diagnóstico" + }, + "tray.menu.about": { + "message": "Ayuda y soporte" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "Documentación" + }, + "tray.menu.troubleshoot": { + "message": "Solucionar problemas" + }, + "tray.menu.downloadLatest": { + "message": "Descargar la última versión" + }, + "tray.menu.installVersion": { + "message": "Instalar la versión {version}" + }, + "tray.menu.guiVersion": { + "message": "GUI: {version}" + }, + "tray.menu.daemonVersion": { + "message": "Daemon: {version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "Salir de NetBird" + }, + "notify.daemonOutdated.title": { + "message": "El servicio de NetBird está desactualizado" + }, + "notify.daemonOutdated.body": { + "message": "Actualice el servicio de NetBird para usar esta aplicación." + }, + "notify.update.title": { + "message": "Actualización de NetBird disponible" + }, + "notify.update.body": { + "message": "NetBird {version} está disponible." + }, + "notify.update.enforcedSuffix": { + "message": " Su administrador requiere esta actualización." + }, + "notify.error.title": { + "message": "Error" + }, + "notify.error.connect": { + "message": "Error al conectar" + }, + "notify.error.disconnect": { + "message": "Error al desconectar" + }, + "notify.error.switchProfile": { + "message": "Error al cambiar a {profile}" + }, + "notify.error.exitNode": { + "message": "Error al actualizar el nodo de salida {name}" + }, + "notify.sessionExpired.title": { + "message": "Sesión de NetBird expirada" + }, + "notify.sessionExpired.body": { + "message": "Su sesión de NetBird ha expirado. Inicie sesión de nuevo." + }, + "notify.sessionWarning.title": { + "message": "La sesión expira pronto" + }, + "notify.sessionWarning.body": { + "message": "Su sesión de NetBird expira en {remaining}. Haga clic en Renovar ahora para renovarla." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "Su sesión de NetBird está a punto de expirar. Haga clic en Renovar ahora para renovarla." + }, + "notify.sessionWarning.extend": { + "message": "Renovar ahora" + }, + "notify.sessionWarning.dismiss": { + "message": "Descartar" + }, + "notify.sessionWarning.failed": { + "message": "Error al renovar la sesión de NetBird" + }, + "notify.sessionWarning.successTitle": { + "message": "Sesión de NetBird renovada" + }, + "notify.sessionWarning.successBody": { + "message": "Su sesión se ha renovado." + }, + "notify.sessionDeadlineRejected.title": { + "message": "Plazo de sesión rechazado" + }, + "notify.sessionDeadlineRejected.body": { + "message": "El servidor envió un plazo de sesión no válido. Inicie sesión de nuevo." + }, + "notify.mdm.policyApplied.title": { + "message": "Configuración de NetBird actualizada" + }, + "notify.mdm.policyApplied.body": { + "message": "Su configuración de NetBird fue actualizada por su política de TI." + }, + "common.cancel": { + "message": "Cancelar" + }, + "common.save": { + "message": "Guardar" + }, + "common.saveChanges": { + "message": "Guardar cambios" + }, + "common.saving": { + "message": "Guardando…" + }, + "common.close": { + "message": "Cerrar" + }, + "common.copy": { + "message": "Copiar" + }, + "common.togglePasswordVisibility": { + "message": "Mostrar u ocultar la contraseña" + }, + "common.increase": { + "message": "Aumentar" + }, + "common.decrease": { + "message": "Disminuir" + }, + "common.delete": { + "message": "Eliminar" + }, + "common.create": { + "message": "Crear" + }, + "common.add": { + "message": "Añadir" + }, + "common.remove": { + "message": "Quitar" + }, + "common.refresh": { + "message": "Actualizar" + }, + "common.loading": { + "message": "Cargando…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "No se encontraron resultados" + }, + "common.noResults.description": { + "message": "No se encontraron resultados. Pruebe con otro término de búsqueda o cambie los filtros." + }, + "notConnected.title": { + "message": "Desconectado" + }, + "notConnected.description": { + "message": "Conéctese primero a NetBird para ver información detallada sobre sus peers, recursos de red y nodos de salida." + }, + "connect.status.disconnected": { + "message": "Desconectado" + }, + "connect.status.connecting": { + "message": "Conectando..." + }, + "connect.status.connected": { + "message": "Conectado" + }, + "connect.status.disconnecting": { + "message": "Desconectando..." + }, + "connect.status.daemonUnavailable": { + "message": "Daemon no disponible" + }, + "connect.status.loginRequired": { + "message": "Inicio de sesión requerido" + }, + "connect.error.loginTitle": { + "message": "Error al iniciar sesión" + }, + "connect.error.connectTitle": { + "message": "Error al conectar" + }, + "connect.error.disconnectTitle": { + "message": "Error al desconectar" + }, + "nav.peers.title": { + "message": "Peers" + }, + "nav.peers.description": { + "message": "{connected} de {total} conectados" + }, + "nav.resources.title": { + "message": "Recursos" + }, + "nav.resources.description": { + "message": "{active} de {total} activos" + }, + "nav.exitNode.title": { + "message": "Nodos de salida" + }, + "nav.exitNode.none": { + "message": "Inactivo" + }, + "nav.exitNode.using": { + "message": "Vía {name}" + }, + "header.openSettings": { + "message": "Abrir configuración" + }, + "header.togglePanel": { + "message": "Mostrar u ocultar el panel lateral" + }, + "profile.selector.loading": { + "message": "Cargando..." + }, + "profile.selector.noProfile": { + "message": "Sin perfil" + }, + "profile.selector.searchPlaceholder": { + "message": "Buscar perfil por nombre..." + }, + "profile.selector.emptyTitle": { + "message": "No se encontraron perfiles" + }, + "profile.selector.emptyDescription": { + "message": "Pruebe con otro término de búsqueda o cree un perfil nuevo." + }, + "profile.selector.newProfile": { + "message": "Nuevo perfil" + }, + "profile.selector.moreOptions": { + "message": "Más opciones" + }, + "profile.selector.deregister": { + "message": "Anular registro" + }, + "profile.selector.delete": { + "message": "Eliminar" + }, + "profile.selector.switchTo": { + "message": "Cambiar a este perfil" + }, + "profile.selector.edit": { + "message": "Editar" + }, + "profile.edit.title": { + "message": "Editar perfil" + }, + "profile.edit.submit": { + "message": "Guardar cambios" + }, + "profile.dialog.title": { + "message": "Introduzca el nombre del perfil" + }, + "profile.dialog.nameLabel": { + "message": "Nombre del perfil" + }, + "profile.dialog.description": { + "message": "Asigne un nombre fácil de identificar a su perfil." + }, + "profile.dialog.placeholder": { + "message": "p. ej. trabajo" + }, + "profile.dialog.submit": { + "message": "Añadir perfil" + }, + "profile.dialog.required": { + "message": "Introduzca un nombre de perfil, p. ej. trabajo, casa" + }, + "profile.dialog.managementHelp": { + "message": "Use NetBird Cloud o su propio servidor." + }, + "profile.dialog.urlUnreachable": { + "message": "No se pudo contactar con este servidor. Compruebe la URL o añada el perfil de todos modos si está seguro de que es correcta." + }, + "header.menu.settings": { + "message": "Configuración..." + }, + "header.menu.defaultView": { + "message": "Vista predeterminada" + }, + "header.menu.advancedView": { + "message": "Vista avanzada" + }, + "header.menu.updateAvailable": { + "message": "Actualización disponible" + }, + "header.menu.open": { + "message": "Abrir menú" + }, + "header.profile.switch": { + "message": "Cambiar de perfil" + }, + "connect.toggle.label": { + "message": "Conmutar conexión NetBird" + }, + "connect.localIp.label": { + "message": "Direcciones IP locales" + }, + "common.search": { + "message": "Buscar" + }, + "common.filter": { + "message": "Filtrar" + }, + "exitNodes.dropdown.trigger": { + "message": "Seleccionar nodo de salida" + }, + "peers.row.label": { + "message": "Abrir detalles de {name}, {status}" + }, + "peers.dialog.title": { + "message": "Detalles del par" + }, + "networks.row.toggle": { + "message": "Conmutar {name}" + }, + "networks.bulk.label": { + "message": "Conmutar todos los recursos visibles" + }, + "settings.nav.label": { + "message": "Secciones de configuración" + }, + "profile.switch.title": { + "message": "¿Cambiar el perfil a «{name}»?" + }, + "profile.switch.message": { + "message": "¿Seguro que desea cambiar de perfil?\nSu perfil actual se desconectará." + }, + "profile.switch.confirm": { + "message": "Confirmar" + }, + "profile.deregister.title": { + "message": "¿Anular el registro del perfil «{name}»?" + }, + "profile.deregister.message": { + "message": "¿Seguro que desea anular el registro de este perfil?\nDeberá iniciar sesión de nuevo para usarlo." + }, + "profile.deregister.confirm": { + "message": "Anular registro" + }, + "profile.delete.title": { + "message": "¿Eliminar el perfil «{name}»?" + }, + "profile.delete.message": { + "message": "¿Seguro que desea eliminar este perfil?\nEsta acción no se puede deshacer." + }, + "profile.delete.disabledActive": { + "message": "Los perfiles activos no se pueden eliminar. Cambie a otro antes de eliminar este perfil." + }, + "profile.delete.disabledDefault": { + "message": "El perfil predeterminado no se puede eliminar." + }, + "profile.error.switchTitle": { + "message": "Error al cambiar de perfil" + }, + "profile.error.deregisterTitle": { + "message": "Error al anular el registro del perfil" + }, + "profile.error.deleteTitle": { + "message": "Error al eliminar el perfil" + }, + "profile.error.createTitle": { + "message": "Error al crear el perfil" + }, + "profile.error.editTitle": { + "message": "Error al editar el perfil" + }, + "profile.error.loadTitle": { + "message": "Error al cargar los perfiles" + }, + "profile.dropdown.activeProfile": { + "message": "Perfil activo" + }, + "profile.dropdown.switchProfile": { + "message": "Cambiar de perfil" + }, + "profile.dropdown.noEmail": { + "message": "Otro" + }, + "profile.dropdown.addProfile": { + "message": "Añadir perfil" + }, + "profile.dropdown.manageProfiles": { + "message": "Gestionar perfiles" + }, + "profile.dropdown.settings": { + "message": "Configuración" + }, + "settings.profiles.section.profiles": { + "message": "Perfiles" + }, + "settings.profiles.intro": { + "message": "Mantenga identidades de NetBird independientes en paralelo, por ejemplo cuentas de trabajo y personales, o distintos servidores de gestión. Añada, anule el registro o elimine perfiles a continuación." + }, + "settings.profiles.addProfile": { + "message": "Añadir perfil" + }, + "settings.profiles.active": { + "message": "Activo" + }, + "settings.profiles.emptyTitle": { + "message": "Sin perfiles" + }, + "settings.profiles.emptyDescription": { + "message": "Cree un perfil para conectarse a un servidor de gestión de NetBird." + }, + "settings.error.loadTitle": { + "message": "Error al cargar la configuración" + }, + "settings.error.saveTitle": { + "message": "Error al guardar la configuración" + }, + "settings.error.debugBundleTitle": { + "message": "Error en el paquete de diagnóstico" + }, + "settings.tabs.general": { + "message": "General" + }, + "settings.tabs.network": { + "message": "Red" + }, + "settings.tabs.security": { + "message": "Seguridad" + }, + "settings.tabs.profiles": { + "message": "Perfiles" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "Avanzado" + }, + "settings.tabs.troubleshooting": { + "message": "Solución de problemas" + }, + "settings.tabs.about": { + "message": "Acerca de" + }, + "settings.tabs.updateAvailable": { + "message": "Actualización disponible" + }, + "settings.general.section.general": { + "message": "General" + }, + "settings.general.section.connection": { + "message": "Conexión" + }, + "settings.general.connectOnStartup.label": { + "message": "Conectar al iniciar" + }, + "settings.general.connectOnStartup.help": { + "message": "Establece una conexión automáticamente cuando se inicia el servicio." + }, + "settings.general.notifications.label": { + "message": "Notificaciones de escritorio" + }, + "settings.general.notifications.help": { + "message": "Muestra notificaciones de escritorio para nuevas actualizaciones y eventos de conexión." + }, + "settings.general.autostart.label": { + "message": "Iniciar la interfaz de NetBird al iniciar sesión" + }, + "settings.general.autostart.help": { + "message": "Inicia la interfaz de NetBird automáticamente al iniciar sesión. Esto afecta solo a la interfaz gráfica, no al servicio en segundo plano." + }, + "settings.general.autostart.errorTitle": { + "message": "Error al cambiar el inicio automático" + }, + "settings.general.language.label": { + "message": "Idioma de la interfaz" + }, + "settings.general.language.help": { + "message": "Elija el idioma de la interfaz de NetBird." + }, + "settings.general.language.search": { + "message": "Buscar idioma…" + }, + "settings.general.language.empty": { + "message": "Ningún idioma coincide." + }, + "settings.general.management.label": { + "message": "Servidor de gestión" + }, + "settings.general.management.help": { + "message": "Conéctese a NetBird Cloud o a su propio servidor de gestión autoalojado. Los cambios reconectarán el cliente." + }, + "settings.general.management.cloud": { + "message": "Cloud" + }, + "settings.general.management.selfHosted": { + "message": "Autoalojado" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "Introduzca una URL válida, p. ej., https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "No se pudo contactar con este servidor. Compruebe la URL o guarde de todos modos si está seguro de que es correcta." + }, + "settings.general.management.switchCloudTitle": { + "message": "¿Cambiar a NetBird Cloud?" + }, + "settings.general.management.switchCloudMessage": { + "message": "Esto desconecta su servidor autoalojado.\nEs posible que deba iniciar sesión de nuevo." + }, + "settings.general.management.switchCloudConfirm": { + "message": "Cambiar a Cloud" + }, + "settings.network.section.connectivity": { + "message": "Conectividad" + }, + "settings.network.section.routingDns": { + "message": "Enrutamiento y DNS" + }, + "settings.network.lazy.label": { + "message": "Conexiones bajo demanda" + }, + "settings.network.lazy.help": { + "message": "En lugar de mantener conexiones permanentes, NetBird las activa bajo demanda según la actividad o la señalización." + }, + "settings.network.monitor.label": { + "message": "Reconectar al cambiar de red" + }, + "settings.network.monitor.help": { + "message": "Monitoriza la red y reconecta automáticamente ante cambios como cambios de Wi-Fi, cambios de Ethernet o la reanudación tras la suspensión." + }, + "settings.network.dns.label": { + "message": "Habilitar DNS" + }, + "settings.network.dns.help": { + "message": "Aplica la configuración de DNS gestionada por NetBird al resolutor del host." + }, + "settings.network.clientRoutes.label": { + "message": "Habilitar rutas de cliente" + }, + "settings.network.clientRoutes.help": { + "message": "Acepta rutas de otros peers para alcanzar sus redes." + }, + "settings.network.serverRoutes.label": { + "message": "Habilitar rutas de servidor" + }, + "settings.network.serverRoutes.help": { + "message": "Anuncia las rutas locales de este host a otros peers." + }, + "settings.network.ipv6.label": { + "message": "Habilitar IPv6" + }, + "settings.network.ipv6.help": { + "message": "Usa direccionamiento IPv6 para la red superpuesta de NetBird." + }, + "settings.security.section.firewall": { + "message": "Cortafuegos" + }, + "settings.security.section.encryption": { + "message": "Cifrado" + }, + "settings.security.blockInbound.label": { + "message": "Bloquear tráfico entrante" + }, + "settings.security.blockInbound.help": { + "message": "Rechaza conexiones no solicitadas de peers hacia este dispositivo y cualquier red que enrute. El tráfico saliente no se ve afectado." + }, + "settings.security.blockLan.label": { + "message": "Bloquear acceso a la LAN" + }, + "settings.security.blockLan.help": { + "message": "Impide que los peers alcancen su red local o sus dispositivos cuando este dispositivo enruta su tráfico." + }, + "settings.security.rosenpass.label": { + "message": "Habilitar resistencia cuántica" + }, + "settings.security.rosenpass.help": { + "message": "Añade un intercambio de claves poscuántico mediante Rosenpass sobre WireGuard®." + }, + "settings.security.rosenpassPermissive.label": { + "message": "Habilitar modo permisivo" + }, + "settings.security.rosenpassPermissive.help": { + "message": "Permite conexiones con peers sin compatibilidad con resistencia cuántica." + }, + "settings.ssh.section.server": { + "message": "Servidor" + }, + "settings.ssh.section.capabilities": { + "message": "Capacidades" + }, + "settings.ssh.section.authentication": { + "message": "Autenticación" + }, + "settings.ssh.server.label": { + "message": "Habilitar el servidor SSH" + }, + "settings.ssh.server.help": { + "message": "Ejecuta el servidor SSH de NetBird en este host para que otros peers puedan conectarse a él." + }, + "settings.ssh.root.label": { + "message": "Permitir inicio de sesión como root" + }, + "settings.ssh.root.help": { + "message": "Permite que los peers inicien sesión como usuario root. Desactívelo para exigir una cuenta sin privilegios." + }, + "settings.ssh.sftp.label": { + "message": "Permitir SFTP" + }, + "settings.ssh.sftp.help": { + "message": "Transfiere archivos de forma segura mediante clientes SFTP o SCP nativos." + }, + "settings.ssh.localForward.label": { + "message": "Reenvío de puertos local" + }, + "settings.ssh.localForward.help": { + "message": "Permite que los peers conectados tunelicen puertos locales hacia servicios accesibles desde este host." + }, + "settings.ssh.remoteForward.label": { + "message": "Reenvío de puertos remoto" + }, + "settings.ssh.remoteForward.help": { + "message": "Permite que los peers conectados expongan puertos de este host de vuelta a su propia máquina." + }, + "settings.ssh.jwt.label": { + "message": "Habilitar autenticación JWT" + }, + "settings.ssh.jwt.help": { + "message": "Verifica cada sesión SSH contra su IdP para la identidad del usuario y la auditoría. Desactívelo para basarse solo en las políticas de ACL de red, útil cuando no hay ningún IdP disponible." + }, + "settings.ssh.jwtTtl.label": { + "message": "TTL de la caché JWT" + }, + "settings.ssh.jwtTtl.help": { + "message": "Cuánto tiempo almacena en caché este cliente un JWT antes de volver a solicitarlo en conexiones SSH salientes. Establézcalo en 0 para desactivar la caché y autenticar en cada conexión." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "s" + }, + "settings.advanced.section.interface": { + "message": "Interfaz" + }, + "settings.advanced.section.security": { + "message": "Seguridad" + }, + "settings.advanced.interfaceName.label": { + "message": "Nombre" + }, + "settings.advanced.interfaceName.error": { + "message": "Use de 1 a 15 letras, dígitos, puntos, guiones o guiones bajos." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "Debe empezar por \"utun\" seguido de un número (p. ej. utun100)." + }, + "settings.advanced.port.label": { + "message": "Puerto" + }, + "settings.advanced.port.error": { + "message": "Introduzca un puerto entre {min} y {max}." + }, + "settings.advanced.port.help": { + "message": "Si se establece en 0, se usará un puerto libre aleatorio." + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "Introduzca un valor de MTU entre {min} y {max}." + }, + "settings.advanced.psk.label": { + "message": "Clave precompartida" + }, + "settings.advanced.psk.help": { + "message": "PSK de WireGuard opcional para cifrado simétrico adicional. No es lo mismo que una clave de instalación de NetBird. Solo se comunicará con peers que usen la misma clave precompartida." + }, + "settings.troubleshooting.section.title": { + "message": "Paquete de diagnóstico" + }, + "settings.troubleshooting.anonymize.label": { + "message": "Anonimizar información sensible" + }, + "settings.troubleshooting.anonymize.help": { + "message": "Oculta las direcciones IP públicas y los dominios ajenos a NetBird de los registros." + }, + "settings.troubleshooting.systemInfo.label": { + "message": "Incluir información del sistema" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "Incluye el OS, el kernel, las interfaces de red y las tablas de enrutamiento." + }, + "settings.troubleshooting.upload.label": { + "message": "Subir el paquete a los servidores de NetBird" + }, + "settings.troubleshooting.upload.help": { + "message": "Devuelve una clave de subida para compartir con el soporte de NetBird." + }, + "settings.troubleshooting.trace.label": { + "message": "Activar registros de seguimiento" + }, + "settings.troubleshooting.trace.help": { + "message": "Eleva el nivel de registro a TRACE y lo restaura después." + }, + "settings.troubleshooting.capture.label": { + "message": "Sesión de captura" + }, + "settings.troubleshooting.capture.help": { + "message": "Vuelve a conectar y espera para que pueda reproducir el problema." + }, + "settings.troubleshooting.packets.label": { + "message": "Capturar paquetes de red" + }, + "settings.troubleshooting.packets.help": { + "message": "Guarda un .pcap del tráfico de red durante la sesión de captura." + }, + "settings.troubleshooting.duration.label": { + "message": "Duración de la captura" + }, + "settings.troubleshooting.duration.help": { + "message": "Cuánto tiempo se ejecuta la sesión de captura." + }, + "settings.troubleshooting.duration.suffix": { + "message": "min" + }, + "settings.troubleshooting.create": { + "message": "Crear paquete" + }, + "settings.troubleshooting.progress.description": { + "message": "Recopilando registros, detalles del sistema y estado de la conexión. Esto suele tardar un momento; mantenga esta ventana abierta hasta que termine." + }, + "settings.troubleshooting.cancelling": { + "message": "Cancelando…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "¡Paquete de diagnóstico subido correctamente!" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "Paquete guardado" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "Comparta la clave de subida de abajo con el soporte de NetBird. También se guardó una copia local en su dispositivo." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "Su paquete de diagnóstico se ha guardado localmente." + }, + "settings.troubleshooting.done.copyKey": { + "message": "Copiar clave" + }, + "settings.troubleshooting.done.openFolder": { + "message": "Abrir carpeta" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "Abrir ubicación del archivo" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "Error en la subida: {reason} El paquete sigue guardado localmente." + }, + "settings.troubleshooting.uploadFailed": { + "message": "Error en la subida. El paquete sigue guardado localmente." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "Reconectando NetBird…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "Capturando registros de depuración" + }, + "settings.troubleshooting.stage.bundling": { + "message": "Generando el paquete de diagnóstico…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "Subiendo a NetBird…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "Cancelando…" + }, + "settings.about.client": { + "message": "NetBird Client v{version}" + }, + "settings.about.clientName": { + "message": "NetBird Client" + }, + "settings.about.development": { + "message": "[Desarrollo]" + }, + "settings.about.gui": { + "message": "GUI v{version}" + }, + "settings.about.guiName": { + "message": "GUI" + }, + "settings.about.copyright": { + "message": "© {year} NetBird. Todos los derechos reservados." + }, + "settings.about.links.imprint": { + "message": "Aviso legal" + }, + "settings.about.links.privacy": { + "message": "Privacidad" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "Términos del servicio" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "Foro" + }, + "settings.about.community.documentation": { + "message": "Documentación" + }, + "settings.about.community.feedback": { + "message": "Comentarios" + }, + "update.banner.message": { + "message": "NetBird {version} está listo para instalar." + }, + "update.banner.later": { + "message": "Más tarde" + }, + "update.banner.installNow": { + "message": "Instalar ahora" + }, + "update.card.versionAvailableDownload": { + "message": "La versión {version} está disponible para descargar." + }, + "update.card.versionAvailableInstall": { + "message": "La versión {version} está disponible para instalar." + }, + "update.card.whatsNew": { + "message": "¿Qué hay de nuevo?" + }, + "update.card.installNow": { + "message": "Instalar ahora" + }, + "update.card.getInstaller": { + "message": "Descargar" + }, + "update.card.autoCheckInterval": { + "message": "NetBird busca actualizaciones en segundo plano." + }, + "update.card.changelog": { + "message": "Registro de cambios" + }, + "update.card.onLatestVersion": { + "message": "Tiene la última versión" + }, + "update.header.tooltip": { + "message": "Actualización disponible" + }, + "update.overlay.updatingVersion": { + "message": "Actualizando NetBird a v{version}" + }, + "update.overlay.updating": { + "message": "Actualizando NetBird" + }, + "update.overlay.description": { + "message": "Hay una versión más reciente disponible y se está instalando. NetBird se reiniciará automáticamente cuando finalice la actualización." + }, + "update.overlay.error.timeoutTitle": { + "message": "La actualización está tardando demasiado" + }, + "update.overlay.error.timeoutDescription": { + "message": "La instalación de {target} tardó demasiado y no finalizó." + }, + "update.overlay.error.canceledTitle": { + "message": "La actualización se detuvo" + }, + "update.overlay.error.canceledDescription": { + "message": "La actualización a {target} se canceló antes de finalizar." + }, + "update.overlay.error.failTitle": { + "message": "No se pudo instalar la actualización" + }, + "update.overlay.error.failDescription": { + "message": "No se pudo instalar {target}." + }, + "update.overlay.error.unknownMessage": { + "message": "error desconocido" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "la nueva versión" + }, + "update.error.loadStateTitle": { + "message": "Error al cargar el estado de la actualización" + }, + "update.error.triggerTitle": { + "message": "Error al iniciar la actualización" + }, + "update.page.versionLine": { + "message": "Actualizando el cliente a: {version}." + }, + "update.page.versionLineGeneric": { + "message": "Actualizando el cliente." + }, + "update.page.outdated": { + "message": "La versión de su cliente es anterior a la versión de actualización automática configurada en Management." + }, + "update.page.status.running": { + "message": "Actualizando" + }, + "update.page.status.timeout": { + "message": "Se agotó el tiempo de la actualización. Inténtelo de nuevo." + }, + "update.page.status.canceled": { + "message": "Actualización cancelada." + }, + "update.page.status.failed": { + "message": "Error en la actualización: {message}" + }, + "update.page.status.unknownError": { + "message": "error de actualización desconocido" + }, + "update.page.failedTitle": { + "message": "Error en la actualización" + }, + "update.page.timeoutMessage": { + "message": "Se agotó el tiempo de la actualización." + }, + "update.page.dontClose": { + "message": "No cierre esta ventana." + }, + "update.page.updating": { + "message": "Actualizando…" + }, + "update.page.complete": { + "message": "Actualización completada" + }, + "update.page.failed": { + "message": "Error en la actualización" + }, + "window.title.settings": { + "message": "Configuración" + }, + "window.title.signIn": { + "message": "Inicio de sesión" + }, + "window.title.sessionExpiration": { + "message": "Sesión a punto de expirar" + }, + "window.title.updating": { + "message": "Actualizando" + }, + "window.title.welcome": { + "message": "Bienvenido a NetBird" + }, + "window.title.error": { + "message": "Error" + }, + "welcome.title": { + "message": "Busque NetBird en su bandeja del sistema" + }, + "welcome.description": { + "message": "NetBird reside en su bandeja del sistema. Haga clic en el icono para conectarse, cambiar de perfil o abrir la configuración." + }, + "welcome.continue": { + "message": "Continuar" + }, + "welcome.back": { + "message": "Atrás" + }, + "welcome.management.title": { + "message": "Configurar NetBird" + }, + "welcome.management.description": { + "message": "Haga clic en Continuar para empezar, o elija Autoalojado si tiene su propio servidor de NetBird." + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud" + }, + "welcome.management.cloud.description": { + "message": "Use nuestro servicio alojado. No requiere configuración." + }, + "welcome.management.selfHosted.title": { + "message": "Autoalojado" + }, + "welcome.management.selfHosted.description": { + "message": "Conéctese a su propio servidor de gestión." + }, + "welcome.management.urlLabel": { + "message": "URL del servidor de gestión" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "Introduzca una URL válida, p. ej., https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "No se pudo contactar con este servidor. Compruebe la URL o su red y, a continuación, continúe si está seguro de que es correcta." + }, + "welcome.management.checking": { + "message": "Comprobando…" + }, + "browserLogin.title": { + "message": "Continúe en su navegador para completar el inicio de sesión" + }, + "browserLogin.notSeeing": { + "message": "¿No ve la pestaña del navegador?" + }, + "browserLogin.tryAgain": { + "message": "Reintentar" + }, + "browserLogin.openFailedTitle": { + "message": "Error al abrir el navegador" + }, + "sessionExpiration.title": { + "message": "La sesión expira pronto" + }, + "sessionExpiration.titleLater": { + "message": "Su sesión expirará" + }, + "sessionExpiration.description": { + "message": "Este dispositivo se desconectará pronto. Renuévela iniciando sesión en el navegador." + }, + "sessionExpiration.descriptionLater": { + "message": "Iniciar sesión en el navegador mantiene este dispositivo conectado a su red." + }, + "sessionExpiration.stay": { + "message": "Renovar sesión" + }, + "sessionExpiration.authenticate": { + "message": "Autenticar" + }, + "sessionExpiration.logout": { + "message": "Cerrar sesión" + }, + "sessionExpiration.expired": { + "message": "Sesión expirada" + }, + "sessionExpiration.expiredDescription": { + "message": "Dispositivo desconectado. Autentíquese iniciando sesión en el navegador para reconectar." + }, + "sessionExpiration.close": { + "message": "Cerrar" + }, + "sessionExpiration.extendFailedTitle": { + "message": "Error al renovar la sesión" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "Error al cerrar sesión" + }, + "peers.search.placeholder": { + "message": "Buscar por nombre o IP" + }, + "peers.filter.all": { + "message": "Todos" + }, + "peers.filter.online": { + "message": "En línea" + }, + "peers.filter.offline": { + "message": "Sin conexión" + }, + "peers.empty.title": { + "message": "No hay peers disponibles" + }, + "peers.empty.description": { + "message": "No tiene ningún peer disponible o no tiene acceso a ninguno de ellos." + }, + "peers.details.domain": { + "message": "Dominio" + }, + "peers.details.netbirdIp": { + "message": "IP de NetBird" + }, + "peers.details.netbirdIpv6": { + "message": "IPv6 de NetBird" + }, + "peers.details.publicKey": { + "message": "Clave pública" + }, + "peers.details.connection": { + "message": "Conexión" + }, + "peers.details.latency": { + "message": "Latencia" + }, + "peers.details.lastHandshake": { + "message": "Último handshake" + }, + "peers.details.statusSince": { + "message": "Última actualización de la conexión" + }, + "peers.details.bytes": { + "message": "Bytes" + }, + "peers.details.bytesSent": { + "message": "Enviados" + }, + "peers.details.bytesReceived": { + "message": "Recibidos" + }, + "peers.details.localIce": { + "message": "ICE local" + }, + "peers.details.remoteIce": { + "message": "ICE remoto" + }, + "peers.details.never": { + "message": "Nunca" + }, + "peers.details.justNow": { + "message": "Justo ahora" + }, + "peers.details.refresh": { + "message": "Actualizar" + }, + "peers.status.connected": { + "message": "Conectado" + }, + "peers.status.connecting": { + "message": "Conectando" + }, + "peers.status.disconnected": { + "message": "Desconectado" + }, + "peers.details.relayAddress": { + "message": "Relay" + }, + "peers.details.networks": { + "message": "Recursos" + }, + "peers.details.relayed": { + "message": "Retransmitida" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "Rosenpass habilitado" + }, + "networks.search.placeholder": { + "message": "Buscar por red o dominio" + }, + "networks.filter.all": { + "message": "Todos" + }, + "networks.filter.active": { + "message": "Activos" + }, + "networks.filter.overlapping": { + "message": "Solapados" + }, + "networks.empty.title": { + "message": "No hay recursos disponibles" + }, + "networks.empty.description": { + "message": "No tiene ningún recurso de red disponible o no tiene acceso a ninguno de ellos." + }, + "networks.selected": { + "message": "Seleccionado" + }, + "networks.unselected": { + "message": "No seleccionado" + }, + "networks.ips.heading": { + "message": "IP resueltas" + }, + "networks.bulk.selectionCount": { + "message": "{selected} de {total} activos" + }, + "networks.bulk.enableAll": { + "message": "Habilitar todos" + }, + "networks.bulk.disableAll": { + "message": "Deshabilitar todos" + }, + "exitNodes.search.placeholder": { + "message": "Buscar nodos de salida" + }, + "exitNodes.none": { + "message": "Ninguno" + }, + "exitNodes.empty.title": { + "message": "No hay nodos de salida disponibles" + }, + "exitNodes.empty.description": { + "message": "No se ha compartido ningún nodo de salida con este peer." + }, + "exitNodes.card.title": { + "message": "Nodo de salida" + }, + "exitNodes.card.statusActive": { + "message": "Activo" + }, + "exitNodes.card.statusInactive": { + "message": "Inactivo" + }, + "exitNodes.dropdown.noneTitle": { + "message": "Ninguno" + }, + "exitNodes.dropdown.noneDescription": { + "message": "Conexión directa sin nodo de salida" + }, + "quickActions.connect": { + "message": "Conectar" + }, + "quickActions.disconnect": { + "message": "Desconectar" + }, + "daemon.unavailable.title": { + "message": "El servicio de NetBird no está en ejecución" + }, + "daemon.unavailable.description": { + "message": "La aplicación se reconectará automáticamente cuando el servicio esté en ejecución." + }, + "daemon.unavailable.docsLink": { + "message": "Documentación" + }, + "daemon.outdated.title": { + "message": "El servicio de NetBird está desactualizado" + }, + "daemon.outdated.description": { + "message": "Actualice el servicio de NetBird para usar esta aplicación." + }, + "error.jwt_clock_skew": { + "message": "Error al iniciar sesión: el reloj de este dispositivo no está sincronizado con el servidor. Sincronice el reloj del sistema e inténtelo de nuevo." + }, + "error.jwt_expired": { + "message": "Su token de inicio de sesión ha expirado. Inicie sesión de nuevo." + }, + "error.jwt_signature_invalid": { + "message": "Error al iniciar sesión: la firma del token no es válida. Póngase en contacto con su administrador." + }, + "error.session_expired": { + "message": "Su sesión ha expirado. Inicie sesión de nuevo." + }, + "error.invalid_setup_key": { + "message": "La clave de instalación falta o no es válida." + }, + "error.permission_denied": { + "message": "El servidor rechazó el inicio de sesión." + }, + "error.daemon_unreachable": { + "message": "El daemon de NetBird no responde. Compruebe que el servicio esté en ejecución." + }, + "error.unknown": { + "message": "La operación falló." + } +} diff --git a/client/ui/i18n/locales/fr/common.json b/client/ui/i18n/locales/fr/common.json new file mode 100644 index 000000000..f51a1ded6 --- /dev/null +++ b/client/ui/i18n/locales/fr/common.json @@ -0,0 +1,1331 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "Déconnecté" + }, + "tray.status.daemonUnavailable": { + "message": "Non démarré" + }, + "tray.status.error": { + "message": "Erreur" + }, + "tray.status.connected": { + "message": "Connecté" + }, + "tray.status.connecting": { + "message": "Connexion" + }, + "tray.status.needsLogin": { + "message": "Connexion requise" + }, + "tray.status.loginFailed": { + "message": "Échec de la connexion" + }, + "tray.status.sessionExpired": { + "message": "Session expirée" + }, + "tray.session.expiresIn": { + "message": "La session expire dans {remaining}" + }, + "tray.session.unit.lessThanMinute": { + "message": "moins d’une minute" + }, + "tray.session.unit.minute": { + "message": "1 minute" + }, + "tray.session.unit.minutes": { + "message": "{count} minutes" + }, + "tray.session.unit.hour": { + "message": "1 heure" + }, + "tray.session.unit.hours": { + "message": "{count} heures" + }, + "tray.session.unit.day": { + "message": "1 jour" + }, + "tray.session.unit.days": { + "message": "{count} jours" + }, + "tray.menu.open": { + "message": "Ouvrir NetBird" + }, + "tray.menu.connect": { + "message": "Se connecter" + }, + "tray.menu.disconnect": { + "message": "Se déconnecter" + }, + "tray.menu.exitNode": { + "message": "Nœud de sortie" + }, + "tray.menu.networks": { + "message": "Ressources" + }, + "tray.menu.profiles": { + "message": "Profils" + }, + "tray.menu.manageProfiles": { + "message": "Gérer les profils" + }, + "tray.menu.settings": { + "message": "Paramètres..." + }, + "tray.menu.debugBundle": { + "message": "Créer un lot de diagnostic" + }, + "tray.menu.about": { + "message": "Aide et assistance" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "Documentation" + }, + "tray.menu.troubleshoot": { + "message": "Dépannage" + }, + "tray.menu.downloadLatest": { + "message": "Télécharger la dernière version" + }, + "tray.menu.installVersion": { + "message": "Installer la version {version}" + }, + "tray.menu.guiVersion": { + "message": "GUI : {version}" + }, + "tray.menu.daemonVersion": { + "message": "Daemon : {version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "Quitter NetBird" + }, + "notify.daemonOutdated.title": { + "message": "Le service NetBird est obsolète" + }, + "notify.daemonOutdated.body": { + "message": "Mettez à jour le service NetBird pour utiliser cette application." + }, + "notify.update.title": { + "message": "Mise à jour de NetBird disponible" + }, + "notify.update.body": { + "message": "NetBird {version} est disponible." + }, + "notify.update.enforcedSuffix": { + "message": " Votre administrateur impose cette mise à jour." + }, + "notify.error.title": { + "message": "Erreur" + }, + "notify.error.connect": { + "message": "Échec de la connexion" + }, + "notify.error.disconnect": { + "message": "Échec de la déconnexion" + }, + "notify.error.switchProfile": { + "message": "Échec du passage à {profile}" + }, + "notify.error.exitNode": { + "message": "Échec de la mise à jour du nœud de sortie {name}" + }, + "notify.sessionExpired.title": { + "message": "Session NetBird expirée" + }, + "notify.sessionExpired.body": { + "message": "Votre session NetBird a expiré. Veuillez vous reconnecter." + }, + "notify.sessionWarning.title": { + "message": "La session expire bientôt" + }, + "notify.sessionWarning.body": { + "message": "Votre session NetBird expire dans {remaining}. Cliquez sur Prolonger pour la renouveler." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "Votre session NetBird est sur le point d’expirer. Cliquez sur Prolonger pour la renouveler." + }, + "notify.sessionWarning.extend": { + "message": "Prolonger" + }, + "notify.sessionWarning.dismiss": { + "message": "Ignorer" + }, + "notify.sessionWarning.failed": { + "message": "Échec de la prolongation de la session NetBird" + }, + "notify.sessionWarning.successTitle": { + "message": "Session NetBird prolongée" + }, + "notify.sessionWarning.successBody": { + "message": "Votre session a été renouvelée." + }, + "notify.sessionDeadlineRejected.title": { + "message": "Échéance de session rejetée" + }, + "notify.sessionDeadlineRejected.body": { + "message": "Le serveur a envoyé une échéance de session invalide. Veuillez vous reconnecter." + }, + "notify.mdm.policyApplied.title": { + "message": "Paramètres NetBird mis à jour" + }, + "notify.mdm.policyApplied.body": { + "message": "Votre configuration NetBird a été mise à jour par votre politique informatique." + }, + "common.cancel": { + "message": "Annuler" + }, + "common.save": { + "message": "Enregistrer" + }, + "common.saveChanges": { + "message": "Enregistrer les modifications" + }, + "common.saving": { + "message": "Enregistrement…" + }, + "common.close": { + "message": "Fermer" + }, + "common.copy": { + "message": "Copier" + }, + "common.togglePasswordVisibility": { + "message": "Afficher ou masquer le mot de passe" + }, + "common.increase": { + "message": "Augmenter" + }, + "common.decrease": { + "message": "Diminuer" + }, + "common.delete": { + "message": "Supprimer" + }, + "common.create": { + "message": "Créer" + }, + "common.add": { + "message": "Ajouter" + }, + "common.remove": { + "message": "Retirer" + }, + "common.refresh": { + "message": "Actualiser" + }, + "common.loading": { + "message": "Chargement…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "Aucun résultat trouvé" + }, + "common.noResults.description": { + "message": "Nous n’avons trouvé aucun résultat. Essayez un autre terme de recherche ou modifiez vos filtres." + }, + "notConnected.title": { + "message": "Déconnecté" + }, + "notConnected.description": { + "message": "Connectez-vous d’abord à NetBird pour consulter les informations détaillées sur vos pairs, vos ressources réseau et vos nœuds de sortie." + }, + "connect.status.disconnected": { + "message": "Déconnecté" + }, + "connect.status.connecting": { + "message": "Connexion..." + }, + "connect.status.connected": { + "message": "Connecté" + }, + "connect.status.disconnecting": { + "message": "Déconnexion..." + }, + "connect.status.daemonUnavailable": { + "message": "Daemon indisponible" + }, + "connect.status.loginRequired": { + "message": "Connexion requise" + }, + "connect.error.loginTitle": { + "message": "Échec de la connexion" + }, + "connect.error.connectTitle": { + "message": "Échec de la connexion" + }, + "connect.error.disconnectTitle": { + "message": "Échec de la déconnexion" + }, + "nav.peers.title": { + "message": "Pairs" + }, + "nav.peers.description": { + "message": "{connected} sur {total} connectés" + }, + "nav.resources.title": { + "message": "Ressources" + }, + "nav.resources.description": { + "message": "{active} sur {total} actives" + }, + "nav.exitNode.title": { + "message": "Nœuds de sortie" + }, + "nav.exitNode.none": { + "message": "Inactif" + }, + "nav.exitNode.using": { + "message": "Via {name}" + }, + "header.openSettings": { + "message": "Ouvrir les paramètres" + }, + "header.togglePanel": { + "message": "Afficher ou masquer le panneau latéral" + }, + "profile.selector.loading": { + "message": "Chargement..." + }, + "profile.selector.noProfile": { + "message": "Aucun profil" + }, + "profile.selector.searchPlaceholder": { + "message": "Rechercher un profil par nom..." + }, + "profile.selector.emptyTitle": { + "message": "Aucun profil trouvé" + }, + "profile.selector.emptyDescription": { + "message": "Essayez un autre terme de recherche ou créez un nouveau profil." + }, + "profile.selector.newProfile": { + "message": "Nouveau profil" + }, + "profile.selector.moreOptions": { + "message": "Plus d’options" + }, + "profile.selector.deregister": { + "message": "Désinscrire" + }, + "profile.selector.delete": { + "message": "Supprimer" + }, + "profile.selector.switchTo": { + "message": "Basculer vers ce profil" + }, + "profile.selector.edit": { + "message": "Modifier" + }, + "profile.edit.title": { + "message": "Modifier le profil" + }, + "profile.edit.submit": { + "message": "Enregistrer les modifications" + }, + "profile.dialog.title": { + "message": "Saisir le nom du profil" + }, + "profile.dialog.nameLabel": { + "message": "Nom du profil" + }, + "profile.dialog.description": { + "message": "Choisissez un nom facilement identifiable pour votre profil." + }, + "profile.dialog.placeholder": { + "message": "ex. travail" + }, + "profile.dialog.submit": { + "message": "Ajouter un profil" + }, + "profile.dialog.required": { + "message": "Veuillez saisir un nom de profil, ex. travail, domicile" + }, + "profile.dialog.managementHelp": { + "message": "Utilisez NetBird Cloud ou votre propre serveur." + }, + "profile.dialog.urlUnreachable": { + "message": "Impossible de joindre ce serveur. Vérifiez l’URL, ou ajoutez le profil quand même si vous êtes sûr qu’elle est correcte." + }, + "header.menu.settings": { + "message": "Paramètres..." + }, + "header.menu.defaultView": { + "message": "Vue par défaut" + }, + "header.menu.advancedView": { + "message": "Vue avancée" + }, + "header.menu.updateAvailable": { + "message": "Mise à jour disponible" + }, + "header.menu.open": { + "message": "Ouvrir le menu" + }, + "header.profile.switch": { + "message": "Changer de profil" + }, + "connect.toggle.label": { + "message": "Activer/désactiver la connexion NetBird" + }, + "connect.localIp.label": { + "message": "Adresses IP locales" + }, + "common.search": { + "message": "Rechercher" + }, + "common.filter": { + "message": "Filtrer" + }, + "exitNodes.dropdown.trigger": { + "message": "Sélectionner un nœud de sortie" + }, + "peers.row.label": { + "message": "Ouvrir les détails pour {name}, {status}" + }, + "peers.dialog.title": { + "message": "Détails du pair" + }, + "networks.row.toggle": { + "message": "Activer/désactiver {name}" + }, + "networks.bulk.label": { + "message": "Activer/désactiver toutes les ressources visibles" + }, + "settings.nav.label": { + "message": "Sections des paramètres" + }, + "profile.switch.title": { + "message": "Basculer vers le profil « {name} » ?" + }, + "profile.switch.message": { + "message": "Voulez-vous vraiment changer de profil ?\nVotre profil actuel sera déconnecté." + }, + "profile.switch.confirm": { + "message": "Confirmer" + }, + "profile.deregister.title": { + "message": "Désinscrire le profil « {name} » ?" + }, + "profile.deregister.message": { + "message": "Voulez-vous vraiment désinscrire ce profil ?\nVous devrez vous reconnecter pour l’utiliser." + }, + "profile.deregister.confirm": { + "message": "Désinscrire" + }, + "profile.delete.title": { + "message": "Supprimer le profil « {name} » ?" + }, + "profile.delete.message": { + "message": "Voulez-vous vraiment supprimer ce profil ?\nCette action est irréversible." + }, + "profile.delete.disabledActive": { + "message": "Les profils actifs ne peuvent pas être supprimés. Basculez vers un autre profil avant de supprimer celui-ci." + }, + "profile.delete.disabledDefault": { + "message": "Le profil par défaut ne peut pas être supprimé." + }, + "profile.error.switchTitle": { + "message": "Échec du changement de profil" + }, + "profile.error.deregisterTitle": { + "message": "Échec de la désinscription du profil" + }, + "profile.error.deleteTitle": { + "message": "Échec de la suppression du profil" + }, + "profile.error.createTitle": { + "message": "Échec de la création du profil" + }, + "profile.error.editTitle": { + "message": "Échec de la modification du profil" + }, + "profile.error.loadTitle": { + "message": "Échec du chargement des profils" + }, + "profile.dropdown.activeProfile": { + "message": "Profil actif" + }, + "profile.dropdown.switchProfile": { + "message": "Changer de profil" + }, + "profile.dropdown.noEmail": { + "message": "Autre" + }, + "profile.dropdown.addProfile": { + "message": "Ajouter un profil" + }, + "profile.dropdown.manageProfiles": { + "message": "Gérer les profils" + }, + "profile.dropdown.settings": { + "message": "Paramètres" + }, + "settings.profiles.section.profiles": { + "message": "Profils" + }, + "settings.profiles.intro": { + "message": "Conservez plusieurs identités NetBird côte à côte, par exemple des comptes professionnels et personnels, ou différents serveurs de gestion. Ajoutez, désinscrivez ou supprimez des profils ci-dessous." + }, + "settings.profiles.addProfile": { + "message": "Ajouter un profil" + }, + "settings.profiles.active": { + "message": "Actif" + }, + "settings.profiles.emptyTitle": { + "message": "Aucun profil" + }, + "settings.profiles.emptyDescription": { + "message": "Créez un profil pour vous connecter à un serveur de gestion NetBird." + }, + "settings.error.loadTitle": { + "message": "Échec du chargement des paramètres" + }, + "settings.error.saveTitle": { + "message": "Échec de l’enregistrement des paramètres" + }, + "settings.error.debugBundleTitle": { + "message": "Échec du lot de diagnostic" + }, + "settings.tabs.general": { + "message": "Général" + }, + "settings.tabs.network": { + "message": "Réseau" + }, + "settings.tabs.security": { + "message": "Sécurité" + }, + "settings.tabs.profiles": { + "message": "Profils" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "Avancé" + }, + "settings.tabs.troubleshooting": { + "message": "Dépannage" + }, + "settings.tabs.about": { + "message": "À propos" + }, + "settings.tabs.updateAvailable": { + "message": "Mise à jour disponible" + }, + "settings.general.section.general": { + "message": "Général" + }, + "settings.general.section.connection": { + "message": "Connexion" + }, + "settings.general.connectOnStartup.label": { + "message": "Se connecter au démarrage" + }, + "settings.general.connectOnStartup.help": { + "message": "Établir automatiquement une connexion au démarrage du service." + }, + "settings.general.notifications.label": { + "message": "Notifications du bureau" + }, + "settings.general.notifications.help": { + "message": "Afficher des notifications du bureau pour les nouvelles mises à jour et les événements de connexion." + }, + "settings.general.autostart.label": { + "message": "Lancer l’interface NetBird à l’ouverture de session" + }, + "settings.general.autostart.help": { + "message": "Démarrer automatiquement l’interface NetBird à l’ouverture de votre session. Cela n’affecte que l’interface graphique, pas le service en arrière-plan." + }, + "settings.general.autostart.errorTitle": { + "message": "Échec de la modification du démarrage automatique" + }, + "settings.general.language.label": { + "message": "Langue d’affichage" + }, + "settings.general.language.help": { + "message": "Choisissez la langue de l’interface NetBird." + }, + "settings.general.language.search": { + "message": "Rechercher une langue…" + }, + "settings.general.language.empty": { + "message": "Aucune langue ne correspond." + }, + "settings.general.management.label": { + "message": "Serveur de gestion" + }, + "settings.general.management.help": { + "message": "Connectez-vous à NetBird Cloud ou à votre propre serveur de gestion auto-hébergé. Les modifications reconnecteront le client." + }, + "settings.general.management.cloud": { + "message": "Cloud" + }, + "settings.general.management.selfHosted": { + "message": "Auto-hébergé" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "Veuillez saisir une URL valide, ex. https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "Impossible de joindre ce serveur. Vérifiez l’URL, ou enregistrez quand même si vous êtes sûr qu’elle est correcte." + }, + "settings.general.management.switchCloudTitle": { + "message": "Basculer vers NetBird Cloud ?" + }, + "settings.general.management.switchCloudMessage": { + "message": "Cela déconnecte votre serveur auto-hébergé.\nVous devrez peut-être vous reconnecter." + }, + "settings.general.management.switchCloudConfirm": { + "message": "Basculer vers Cloud" + }, + "settings.network.section.connectivity": { + "message": "Connectivité" + }, + "settings.network.section.routingDns": { + "message": "Routage et DNS" + }, + "settings.network.lazy.label": { + "message": "Connexions à la demande" + }, + "settings.network.lazy.help": { + "message": "Au lieu de maintenir des connexions permanentes, NetBird les active à la demande en fonction de l’activité ou de la signalisation." + }, + "settings.network.monitor.label": { + "message": "Reconnecter en cas de changement de réseau" + }, + "settings.network.monitor.help": { + "message": "Surveiller le réseau et se reconnecter automatiquement lors de changements tels qu’un basculement Wi-Fi, une modification Ethernet ou une reprise après la mise en veille." + }, + "settings.network.dns.label": { + "message": "Activer le DNS" + }, + "settings.network.dns.help": { + "message": "Appliquer les paramètres DNS gérés par NetBird au résolveur de l’hôte." + }, + "settings.network.clientRoutes.label": { + "message": "Activer les routes client" + }, + "settings.network.clientRoutes.help": { + "message": "Accepter les routes d’autres pairs pour atteindre leurs réseaux." + }, + "settings.network.serverRoutes.label": { + "message": "Activer les routes serveur" + }, + "settings.network.serverRoutes.help": { + "message": "Annoncer les routes locales de cet hôte aux autres pairs." + }, + "settings.network.ipv6.label": { + "message": "Activer IPv6" + }, + "settings.network.ipv6.help": { + "message": "Utiliser l’adressage IPv6 pour le réseau overlay NetBird." + }, + "settings.security.section.firewall": { + "message": "Pare-feu" + }, + "settings.security.section.encryption": { + "message": "Chiffrement" + }, + "settings.security.blockInbound.label": { + "message": "Bloquer le trafic entrant" + }, + "settings.security.blockInbound.help": { + "message": "Rejeter les connexions non sollicitées des pairs vers cet appareil et les réseaux qu’il route. Le trafic sortant n’est pas affecté." + }, + "settings.security.blockLan.label": { + "message": "Bloquer l’accès au LAN" + }, + "settings.security.blockLan.help": { + "message": "Empêcher les pairs d’atteindre votre réseau local ou ses appareils lorsque cet appareil route leur trafic." + }, + "settings.security.rosenpass.label": { + "message": "Activer la résistance quantique" + }, + "settings.security.rosenpass.help": { + "message": "Ajouter un échange de clés post-quantique via Rosenpass au-dessus de WireGuard®." + }, + "settings.security.rosenpassPermissive.label": { + "message": "Activer le mode permissif" + }, + "settings.security.rosenpassPermissive.help": { + "message": "Autoriser les connexions vers des pairs sans prise en charge de la résistance quantique." + }, + "settings.ssh.section.server": { + "message": "Serveur" + }, + "settings.ssh.section.capabilities": { + "message": "Fonctionnalités" + }, + "settings.ssh.section.authentication": { + "message": "Authentification" + }, + "settings.ssh.server.label": { + "message": "Activer le serveur SSH" + }, + "settings.ssh.server.help": { + "message": "Exécuter le serveur SSH NetBird sur cet hôte afin que d’autres pairs puissent s’y connecter." + }, + "settings.ssh.root.label": { + "message": "Autoriser la connexion en root" + }, + "settings.ssh.root.help": { + "message": "Permettre aux pairs de se connecter en tant qu’utilisateur root. Désactivez pour exiger un compte non privilégié." + }, + "settings.ssh.sftp.label": { + "message": "Autoriser SFTP" + }, + "settings.ssh.sftp.help": { + "message": "Transférer des fichiers en toute sécurité à l’aide de clients SFTP ou SCP natifs." + }, + "settings.ssh.localForward.label": { + "message": "Redirection de port locale" + }, + "settings.ssh.localForward.help": { + "message": "Permettre aux pairs connectés de tunneliser des ports locaux vers des services accessibles depuis cet hôte." + }, + "settings.ssh.remoteForward.label": { + "message": "Redirection de port distante" + }, + "settings.ssh.remoteForward.help": { + "message": "Permettre aux pairs connectés d’exposer des ports de cet hôte vers leur propre machine." + }, + "settings.ssh.jwt.label": { + "message": "Activer l’authentification JWT" + }, + "settings.ssh.jwt.help": { + "message": "Vérifier chaque session SSH auprès de votre IdP pour l’identité de l’utilisateur et l’audit. Désactivez pour vous appuyer uniquement sur les politiques ACL réseau, utile lorsqu’aucun IdP n’est disponible." + }, + "settings.ssh.jwtTtl.label": { + "message": "TTL du cache JWT" + }, + "settings.ssh.jwtTtl.help": { + "message": "Durée pendant laquelle ce client met en cache un JWT avant de redemander lors des connexions SSH sortantes. Définissez 0 pour désactiver la mise en cache et vous authentifier à chaque connexion." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "s" + }, + "settings.advanced.section.interface": { + "message": "Interface" + }, + "settings.advanced.section.security": { + "message": "Sécurité" + }, + "settings.advanced.interfaceName.label": { + "message": "Nom" + }, + "settings.advanced.interfaceName.error": { + "message": "Utilisez 1 à 15 lettres, chiffres, points, tirets ou traits de soulignement." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "Doit commencer par « utun » suivi d’un nombre (ex. utun100)." + }, + "settings.advanced.port.label": { + "message": "Port" + }, + "settings.advanced.port.error": { + "message": "Saisissez un port compris entre {min} et {max}." + }, + "settings.advanced.port.help": { + "message": "Si la valeur est 0, un port libre aléatoire sera utilisé." + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "Saisissez une valeur MTU comprise entre {min} et {max}." + }, + "settings.advanced.psk.label": { + "message": "Clé pré-partagée" + }, + "settings.advanced.psk.help": { + "message": "PSK WireGuard facultative pour un chiffrement symétrique supplémentaire. Différente d’une clé d’installation NetBird. Vous ne communiquerez qu’avec les pairs utilisant la même clé pré-partagée." + }, + "settings.troubleshooting.section.title": { + "message": "Lot de diagnostic" + }, + "settings.troubleshooting.anonymize.label": { + "message": "Anonymiser les informations sensibles" + }, + "settings.troubleshooting.anonymize.help": { + "message": "Masque les adresses IP publiques et les domaines non-NetBird dans les journaux." + }, + "settings.troubleshooting.systemInfo.label": { + "message": "Inclure les informations système" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "Inclure l’OS, le noyau, les interfaces réseau et les tables de routage." + }, + "settings.troubleshooting.upload.label": { + "message": "Envoyer le lot aux serveurs NetBird" + }, + "settings.troubleshooting.upload.help": { + "message": "Renvoie une clé d’envoi à partager avec l’assistance NetBird." + }, + "settings.troubleshooting.trace.label": { + "message": "Activer les journaux trace" + }, + "settings.troubleshooting.trace.help": { + "message": "Élève le niveau de journalisation à TRACE, puis le rétablit ensuite." + }, + "settings.troubleshooting.capture.label": { + "message": "Session de capture" + }, + "settings.troubleshooting.capture.help": { + "message": "Se reconnecte et attend pour que vous puissiez reproduire le problème." + }, + "settings.troubleshooting.packets.label": { + "message": "Capturer les paquets réseau" + }, + "settings.troubleshooting.packets.help": { + "message": "Enregistre un .pcap du trafic réseau pendant la session de capture." + }, + "settings.troubleshooting.duration.label": { + "message": "Durée de capture" + }, + "settings.troubleshooting.duration.help": { + "message": "Durée d’exécution de la session de capture." + }, + "settings.troubleshooting.duration.suffix": { + "message": "min" + }, + "settings.troubleshooting.create": { + "message": "Créer le lot" + }, + "settings.troubleshooting.progress.description": { + "message": "Collecte des journaux, des détails système et de l’état de connexion. Cela prend généralement un instant — gardez cette fenêtre ouverte jusqu’à la fin." + }, + "settings.troubleshooting.cancelling": { + "message": "Annulation…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "Lot de diagnostic envoyé avec succès !" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "Lot enregistré" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "Partagez la clé d’envoi ci-dessous avec l’assistance NetBird. Une copie locale a également été enregistrée sur votre appareil." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "Votre lot de diagnostic a été enregistré localement." + }, + "settings.troubleshooting.done.copyKey": { + "message": "Copier la clé" + }, + "settings.troubleshooting.done.openFolder": { + "message": "Ouvrir le dossier" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "Ouvrir l’emplacement du fichier" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "Échec de l’envoi : {reason} Le lot reste enregistré localement." + }, + "settings.troubleshooting.uploadFailed": { + "message": "Échec de l’envoi. Le lot reste enregistré localement." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "Reconnexion de NetBird…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "Capture des journaux de débogage" + }, + "settings.troubleshooting.stage.bundling": { + "message": "Génération du lot de diagnostic…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "Envoi vers NetBird…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "Annulation…" + }, + "settings.about.client": { + "message": "Client NetBird v{version}" + }, + "settings.about.clientName": { + "message": "Client NetBird" + }, + "settings.about.development": { + "message": "[Développement]" + }, + "settings.about.gui": { + "message": "GUI v{version}" + }, + "settings.about.guiName": { + "message": "GUI" + }, + "settings.about.copyright": { + "message": "© {year} NetBird. Tous droits réservés." + }, + "settings.about.links.imprint": { + "message": "Mentions légales" + }, + "settings.about.links.privacy": { + "message": "Confidentialité" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "Conditions d’utilisation" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "Forum" + }, + "settings.about.community.documentation": { + "message": "Documentation" + }, + "settings.about.community.feedback": { + "message": "Retour d’expérience" + }, + "update.banner.message": { + "message": "NetBird {version} est prêt à être installé." + }, + "update.banner.later": { + "message": "Plus tard" + }, + "update.banner.installNow": { + "message": "Installer maintenant" + }, + "update.card.versionAvailableDownload": { + "message": "La version {version} est disponible au téléchargement." + }, + "update.card.versionAvailableInstall": { + "message": "La version {version} est disponible à l’installation." + }, + "update.card.whatsNew": { + "message": "Quoi de neuf ?" + }, + "update.card.installNow": { + "message": "Installer maintenant" + }, + "update.card.getInstaller": { + "message": "Télécharger" + }, + "update.card.autoCheckInterval": { + "message": "NetBird vérifie les mises à jour en arrière-plan." + }, + "update.card.changelog": { + "message": "Journal des modifications" + }, + "update.card.onLatestVersion": { + "message": "Vous utilisez la dernière version" + }, + "update.header.tooltip": { + "message": "Mise à jour disponible" + }, + "update.overlay.updatingVersion": { + "message": "Mise à jour de NetBird vers v{version}" + }, + "update.overlay.updating": { + "message": "Mise à jour de NetBird" + }, + "update.overlay.description": { + "message": "Une version plus récente est disponible et en cours d’installation. NetBird redémarrera automatiquement une fois la mise à jour terminée." + }, + "update.overlay.error.timeoutTitle": { + "message": "La mise à jour prend trop de temps" + }, + "update.overlay.error.timeoutDescription": { + "message": "L’installation de {target} a pris trop de temps et ne s’est pas terminée." + }, + "update.overlay.error.canceledTitle": { + "message": "La mise à jour a été interrompue" + }, + "update.overlay.error.canceledDescription": { + "message": "La mise à jour vers {target} a été annulée avant de se terminer." + }, + "update.overlay.error.failTitle": { + "message": "Impossible d’installer la mise à jour" + }, + "update.overlay.error.failDescription": { + "message": "Impossible d’installer {target}." + }, + "update.overlay.error.unknownMessage": { + "message": "erreur inconnue" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "la nouvelle version" + }, + "update.error.loadStateTitle": { + "message": "Échec du chargement de l’état de mise à jour" + }, + "update.error.triggerTitle": { + "message": "Échec du démarrage de la mise à jour" + }, + "update.page.versionLine": { + "message": "Mise à jour du client vers : {version}." + }, + "update.page.versionLineGeneric": { + "message": "Mise à jour du client." + }, + "update.page.outdated": { + "message": "La version de votre client est antérieure à la version de mise à jour automatique définie dans Management." + }, + "update.page.status.running": { + "message": "Mise à jour en cours" + }, + "update.page.status.timeout": { + "message": "La mise à jour a expiré. Veuillez réessayer." + }, + "update.page.status.canceled": { + "message": "Mise à jour annulée." + }, + "update.page.status.failed": { + "message": "Échec de la mise à jour : {message}" + }, + "update.page.status.unknownError": { + "message": "erreur de mise à jour inconnue" + }, + "update.page.failedTitle": { + "message": "Échec de la mise à jour" + }, + "update.page.timeoutMessage": { + "message": "La mise à jour a expiré." + }, + "update.page.dontClose": { + "message": "Veuillez ne pas fermer cette fenêtre." + }, + "update.page.updating": { + "message": "Mise à jour…" + }, + "update.page.complete": { + "message": "Mise à jour terminée" + }, + "update.page.failed": { + "message": "Échec de la mise à jour" + }, + "window.title.settings": { + "message": "Paramètres" + }, + "window.title.signIn": { + "message": "Connexion" + }, + "window.title.sessionExpiration": { + "message": "Expiration de session" + }, + "window.title.updating": { + "message": "Mise à jour" + }, + "window.title.welcome": { + "message": "Bienvenue dans NetBird" + }, + "window.title.error": { + "message": "Erreur" + }, + "welcome.title": { + "message": "Cherchez NetBird dans votre barre d’état système" + }, + "welcome.description": { + "message": "NetBird se trouve dans votre barre d’état système. Cliquez sur l’icône pour vous connecter, changer de profil ou ouvrir les paramètres." + }, + "welcome.continue": { + "message": "Continuer" + }, + "welcome.back": { + "message": "Retour" + }, + "welcome.management.title": { + "message": "Configurer NetBird" + }, + "welcome.management.description": { + "message": "Cliquez sur Continuer pour commencer, ou choisissez Auto-hébergé si vous disposez de votre propre serveur NetBird." + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud" + }, + "welcome.management.cloud.description": { + "message": "Utilisez notre service hébergé. Aucune configuration requise." + }, + "welcome.management.selfHosted.title": { + "message": "Auto-hébergé" + }, + "welcome.management.selfHosted.description": { + "message": "Connectez-vous à votre propre serveur de gestion." + }, + "welcome.management.urlLabel": { + "message": "URL du serveur de gestion" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "Veuillez saisir une URL valide, ex. https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "Impossible de joindre ce serveur. Vérifiez l’URL ou votre réseau, puis continuez si vous êtes sûr qu’elle est correcte." + }, + "welcome.management.checking": { + "message": "Vérification…" + }, + "browserLogin.title": { + "message": "Continuez dans votre navigateur pour terminer la connexion" + }, + "browserLogin.notSeeing": { + "message": "Vous ne voyez pas l’onglet du navigateur ?" + }, + "browserLogin.tryAgain": { + "message": "Réessayer" + }, + "browserLogin.openFailedTitle": { + "message": "Échec de l’ouverture du navigateur" + }, + "sessionExpiration.title": { + "message": "La session expire bientôt" + }, + "sessionExpiration.titleLater": { + "message": "Votre session va expirer" + }, + "sessionExpiration.description": { + "message": "Cet appareil sera bientôt déconnecté. Renouvelez via une connexion dans le navigateur." + }, + "sessionExpiration.descriptionLater": { + "message": "Une connexion dans le navigateur maintient cet appareil connecté à votre réseau." + }, + "sessionExpiration.stay": { + "message": "Renouveler la session" + }, + "sessionExpiration.authenticate": { + "message": "S’authentifier" + }, + "sessionExpiration.logout": { + "message": "Se déconnecter" + }, + "sessionExpiration.expired": { + "message": "Session expirée" + }, + "sessionExpiration.expiredDescription": { + "message": "Appareil déconnecté. Authentifiez-vous via une connexion dans le navigateur pour vous reconnecter." + }, + "sessionExpiration.close": { + "message": "Fermer" + }, + "sessionExpiration.extendFailedTitle": { + "message": "Échec de la prolongation de la session" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "Échec de la déconnexion" + }, + "peers.search.placeholder": { + "message": "Rechercher par nom ou IP" + }, + "peers.filter.all": { + "message": "Tous" + }, + "peers.filter.online": { + "message": "En ligne" + }, + "peers.filter.offline": { + "message": "Hors ligne" + }, + "peers.empty.title": { + "message": "Aucun pair disponible" + }, + "peers.empty.description": { + "message": "Soit vous n’avez aucun pair disponible, soit vous n’y avez pas accès." + }, + "peers.details.domain": { + "message": "Domaine" + }, + "peers.details.netbirdIp": { + "message": "IP NetBird" + }, + "peers.details.netbirdIpv6": { + "message": "IPv6 NetBird" + }, + "peers.details.publicKey": { + "message": "Clé publique" + }, + "peers.details.connection": { + "message": "Connexion" + }, + "peers.details.latency": { + "message": "Latence" + }, + "peers.details.lastHandshake": { + "message": "Dernier handshake" + }, + "peers.details.statusSince": { + "message": "Dernière mise à jour de connexion" + }, + "peers.details.bytes": { + "message": "Octets" + }, + "peers.details.bytesSent": { + "message": "Envoyés" + }, + "peers.details.bytesReceived": { + "message": "Reçus" + }, + "peers.details.localIce": { + "message": "ICE local" + }, + "peers.details.remoteIce": { + "message": "ICE distant" + }, + "peers.details.never": { + "message": "Jamais" + }, + "peers.details.justNow": { + "message": "À l’instant" + }, + "peers.details.refresh": { + "message": "Actualiser" + }, + "peers.status.connected": { + "message": "Connecté" + }, + "peers.status.connecting": { + "message": "Connexion" + }, + "peers.status.disconnected": { + "message": "Déconnecté" + }, + "peers.details.relayAddress": { + "message": "Relais" + }, + "peers.details.networks": { + "message": "Ressources" + }, + "peers.details.relayed": { + "message": "Relayée" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "Rosenpass activé" + }, + "networks.search.placeholder": { + "message": "Rechercher par réseau ou domaine" + }, + "networks.filter.all": { + "message": "Toutes" + }, + "networks.filter.active": { + "message": "Actives" + }, + "networks.filter.overlapping": { + "message": "Chevauchantes" + }, + "networks.empty.title": { + "message": "Aucune ressource disponible" + }, + "networks.empty.description": { + "message": "Soit vous n’avez aucune ressource réseau disponible, soit vous n’y avez pas accès." + }, + "networks.selected": { + "message": "Sélectionnée" + }, + "networks.unselected": { + "message": "Non sélectionnée" + }, + "networks.ips.heading": { + "message": "IP résolues" + }, + "networks.bulk.selectionCount": { + "message": "{selected} sur {total} actives" + }, + "networks.bulk.enableAll": { + "message": "Tout activer" + }, + "networks.bulk.disableAll": { + "message": "Tout désactiver" + }, + "exitNodes.search.placeholder": { + "message": "Rechercher des nœuds de sortie" + }, + "exitNodes.none": { + "message": "Aucun" + }, + "exitNodes.empty.title": { + "message": "Aucun nœud de sortie disponible" + }, + "exitNodes.empty.description": { + "message": "Aucun nœud de sortie n’a été partagé avec ce pair." + }, + "exitNodes.card.title": { + "message": "Nœud de sortie" + }, + "exitNodes.card.statusActive": { + "message": "Actif" + }, + "exitNodes.card.statusInactive": { + "message": "Inactif" + }, + "exitNodes.dropdown.noneTitle": { + "message": "Aucun" + }, + "exitNodes.dropdown.noneDescription": { + "message": "Connexion directe sans nœud de sortie" + }, + "quickActions.connect": { + "message": "Se connecter" + }, + "quickActions.disconnect": { + "message": "Se déconnecter" + }, + "daemon.unavailable.title": { + "message": "Le service NetBird n’est pas en cours d’exécution" + }, + "daemon.unavailable.description": { + "message": "L’application se reconnectera automatiquement une fois le service en cours d’exécution." + }, + "daemon.unavailable.docsLink": { + "message": "Documentation" + }, + "daemon.outdated.title": { + "message": "Le service NetBird est obsolète" + }, + "daemon.outdated.description": { + "message": "Mettez à jour le service NetBird pour utiliser cette application." + }, + "error.jwt_clock_skew": { + "message": "Échec de la connexion : l’horloge de cet appareil n’est pas synchronisée avec le serveur. Veuillez synchroniser l’horloge de votre système et réessayer." + }, + "error.jwt_expired": { + "message": "Votre jeton de connexion a expiré. Veuillez vous reconnecter." + }, + "error.jwt_signature_invalid": { + "message": "Échec de la connexion : la signature du jeton est invalide. Veuillez contacter votre administrateur." + }, + "error.session_expired": { + "message": "Votre session a expiré. Veuillez vous reconnecter." + }, + "error.invalid_setup_key": { + "message": "La clé d’installation est manquante ou invalide." + }, + "error.permission_denied": { + "message": "La connexion a été rejetée par le serveur." + }, + "error.daemon_unreachable": { + "message": "Le daemon NetBird ne répond pas. Veuillez vérifier que le service est en cours d’exécution." + }, + "error.unknown": { + "message": "L’opération a échoué." + } +} diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json new file mode 100644 index 000000000..37dd61d0d --- /dev/null +++ b/client/ui/i18n/locales/hu/common.json @@ -0,0 +1,1331 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "Lecsatlakozva" + }, + "tray.status.daemonUnavailable": { + "message": "Nem fut" + }, + "tray.status.error": { + "message": "Hiba" + }, + "tray.status.connected": { + "message": "Csatlakozva" + }, + "tray.status.connecting": { + "message": "Csatlakozás" + }, + "tray.status.needsLogin": { + "message": "Bejelentkezés szükséges" + }, + "tray.status.loginFailed": { + "message": "Sikertelen bejelentkezés" + }, + "tray.status.sessionExpired": { + "message": "Munkamenet lejárt" + }, + "tray.session.expiresIn": { + "message": "Munkamenet lejár {remaining} múlva" + }, + "tray.session.unit.lessThanMinute": { + "message": "egy percnél kevesebb" + }, + "tray.session.unit.minute": { + "message": "1 perc" + }, + "tray.session.unit.minutes": { + "message": "{count} perc" + }, + "tray.session.unit.hour": { + "message": "1 óra" + }, + "tray.session.unit.hours": { + "message": "{count} óra" + }, + "tray.session.unit.day": { + "message": "1 nap" + }, + "tray.session.unit.days": { + "message": "{count} nap" + }, + "tray.menu.open": { + "message": "NetBird megnyitása" + }, + "tray.menu.connect": { + "message": "Csatlakozás" + }, + "tray.menu.disconnect": { + "message": "Bontás" + }, + "tray.menu.exitNode": { + "message": "Exit Node" + }, + "tray.menu.networks": { + "message": "Erőforrások" + }, + "tray.menu.profiles": { + "message": "Profilok" + }, + "tray.menu.manageProfiles": { + "message": "Profilok kezelése" + }, + "tray.menu.settings": { + "message": "Beállítások…" + }, + "tray.menu.debugBundle": { + "message": "Hibakeresési csomag készítése" + }, + "tray.menu.about": { + "message": "Súgó és támogatás" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "Dokumentáció" + }, + "tray.menu.troubleshoot": { + "message": "Hibakeresés" + }, + "tray.menu.downloadLatest": { + "message": "Legfrissebb verzió letöltése" + }, + "tray.menu.installVersion": { + "message": "{version} verzió telepítése" + }, + "tray.menu.guiVersion": { + "message": "GUI: {version}" + }, + "tray.menu.daemonVersion": { + "message": "Daemon: {version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "NetBird bezárása" + }, + "notify.daemonOutdated.title": { + "message": "A NetBird szolgáltatás elavult" + }, + "notify.daemonOutdated.body": { + "message": "Frissítsd a NetBird szolgáltatást az alkalmazás használatához." + }, + "notify.update.title": { + "message": "NetBird frissítés elérhető" + }, + "notify.update.body": { + "message": "Elérhető a NetBird {version}." + }, + "notify.update.enforcedSuffix": { + "message": " A rendszergazda kötelezővé tette ezt a frissítést." + }, + "notify.error.title": { + "message": "Hiba" + }, + "notify.error.connect": { + "message": "Csatlakozás sikertelen" + }, + "notify.error.disconnect": { + "message": "Bontás sikertelen" + }, + "notify.error.switchProfile": { + "message": "Átváltás sikertelen erre: {profile}" + }, + "notify.error.exitNode": { + "message": "Az Exit Node frissítése sikertelen: {name}" + }, + "notify.sessionExpired.title": { + "message": "NetBird munkamenet lejárt" + }, + "notify.sessionExpired.body": { + "message": "A NetBird munkamenet lejárt. Kérjük, jelentkezzen be újra." + }, + "notify.sessionWarning.title": { + "message": "Munkamenet hamarosan lejár" + }, + "notify.sessionWarning.body": { + "message": "A NetBird munkamenet {remaining} múlva lejár. Kattintson a Meghosszabbítás gombra a megújításhoz." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "A NetBird munkamenet hamarosan lejár. Kattintson a Meghosszabbítás gombra a megújításhoz." + }, + "notify.sessionWarning.extend": { + "message": "Meghosszabbítás" + }, + "notify.sessionWarning.dismiss": { + "message": "Elvetés" + }, + "notify.sessionWarning.failed": { + "message": "A NetBird munkamenet meghosszabbítása sikertelen" + }, + "notify.sessionWarning.successTitle": { + "message": "NetBird munkamenet meghosszabbítva" + }, + "notify.sessionWarning.successBody": { + "message": "A munkamenet frissítve." + }, + "notify.sessionDeadlineRejected.title": { + "message": "Munkamenet-határidő elutasítva" + }, + "notify.sessionDeadlineRejected.body": { + "message": "A szerver érvénytelen munkamenet-határidőt küldött. Kérjük, jelentkezzen be újra." + }, + "notify.mdm.policyApplied.title": { + "message": "NetBird beállítások frissítve" + }, + "notify.mdm.policyApplied.body": { + "message": "A NetBird konfigurációt az IT-szabályzat frissítette." + }, + "common.cancel": { + "message": "Mégse" + }, + "common.save": { + "message": "Mentés" + }, + "common.saveChanges": { + "message": "Módosítások mentése" + }, + "common.saving": { + "message": "Mentés…" + }, + "common.close": { + "message": "Bezárás" + }, + "common.copy": { + "message": "Másolás" + }, + "common.togglePasswordVisibility": { + "message": "Jelszó láthatóságának váltása" + }, + "common.increase": { + "message": "Növelés" + }, + "common.decrease": { + "message": "Csökkentés" + }, + "common.delete": { + "message": "Törlés" + }, + "common.create": { + "message": "Létrehozás" + }, + "common.add": { + "message": "Hozzáadás" + }, + "common.remove": { + "message": "Eltávolítás" + }, + "common.refresh": { + "message": "Frissítés" + }, + "common.loading": { + "message": "Betöltés…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "Nincs találat" + }, + "common.noResults.description": { + "message": "Nem találtunk eredményt. Próbáljon meg másik keresési kifejezést, vagy módosítsa a szűrőket." + }, + "notConnected.title": { + "message": "Lecsatlakozva" + }, + "notConnected.description": { + "message": "Csatlakozzon először a NetBirdhöz, hogy részletes információkat láthasson a Peerekről, a hálózati erőforrásokról és az Exit Node-okról." + }, + "connect.status.disconnected": { + "message": "Lecsatlakozva" + }, + "connect.status.connecting": { + "message": "Csatlakozás…" + }, + "connect.status.connected": { + "message": "Csatlakozva" + }, + "connect.status.disconnecting": { + "message": "Lecsatlakozás…" + }, + "connect.status.daemonUnavailable": { + "message": "Daemon nem elérhető" + }, + "connect.status.loginRequired": { + "message": "Bejelentkezés szükséges" + }, + "connect.error.loginTitle": { + "message": "Bejelentkezés sikertelen" + }, + "connect.error.connectTitle": { + "message": "Csatlakozás sikertelen" + }, + "connect.error.disconnectTitle": { + "message": "Bontás sikertelen" + }, + "nav.peers.title": { + "message": "Peerek" + }, + "nav.peers.description": { + "message": "{connected} / {total} csatlakoztatva" + }, + "nav.resources.title": { + "message": "Erőforrások" + }, + "nav.resources.description": { + "message": "{active} / {total} aktív" + }, + "nav.exitNode.title": { + "message": "Exit Node-ok" + }, + "nav.exitNode.none": { + "message": "Nem aktív" + }, + "nav.exitNode.using": { + "message": "Ezen át: {name}" + }, + "header.openSettings": { + "message": "Beállítások megnyitása" + }, + "header.togglePanel": { + "message": "Oldalsó panel váltása" + }, + "profile.selector.loading": { + "message": "Betöltés…" + }, + "profile.selector.noProfile": { + "message": "Nincs profil" + }, + "profile.selector.searchPlaceholder": { + "message": "Profil keresése név alapján…" + }, + "profile.selector.emptyTitle": { + "message": "Nem található profil" + }, + "profile.selector.emptyDescription": { + "message": "Próbáljon más keresőkifejezést, vagy hozzon létre új profilt." + }, + "profile.selector.newProfile": { + "message": "Új profil" + }, + "profile.selector.moreOptions": { + "message": "További műveletek" + }, + "profile.selector.deregister": { + "message": "Leválasztás" + }, + "profile.selector.delete": { + "message": "Profil törlése" + }, + "profile.selector.switchTo": { + "message": "Váltás erre a profilra" + }, + "profile.selector.edit": { + "message": "Szerkesztés" + }, + "profile.edit.title": { + "message": "Profil szerkesztése" + }, + "profile.edit.submit": { + "message": "Módosítások mentése" + }, + "profile.dialog.title": { + "message": "Új profil" + }, + "profile.dialog.nameLabel": { + "message": "Profilnév" + }, + "profile.dialog.description": { + "message": "Adjon profiljának egy könnyen azonosítható nevet." + }, + "profile.dialog.placeholder": { + "message": "pl. Munka" + }, + "profile.dialog.submit": { + "message": "Profil hozzáadása" + }, + "profile.dialog.required": { + "message": "Adjon meg egy profilnevet, pl. Munka, Otthon" + }, + "profile.dialog.managementHelp": { + "message": "NetBird Cloud vagy saját kiszolgáló." + }, + "profile.dialog.urlUnreachable": { + "message": "A szerver nem érhető el. Ellenőrizze az URL-t, vagy adja hozzá a profilt, ha biztos benne, hogy helyes." + }, + "header.menu.settings": { + "message": "Beállítások…" + }, + "header.menu.defaultView": { + "message": "Alapnézet" + }, + "header.menu.advancedView": { + "message": "Speciális nézet" + }, + "header.menu.updateAvailable": { + "message": "Frissítés elérhető" + }, + "header.menu.open": { + "message": "Menü megnyitása" + }, + "header.profile.switch": { + "message": "Profilváltás" + }, + "connect.toggle.label": { + "message": "NetBird-kapcsolat be/ki" + }, + "connect.localIp.label": { + "message": "Helyi IP-címek" + }, + "common.search": { + "message": "Keresés" + }, + "common.filter": { + "message": "Szűrés" + }, + "exitNodes.dropdown.trigger": { + "message": "Kilépőcsomópont kiválasztása" + }, + "peers.row.label": { + "message": "Részletek megnyitása: {name}, {status}" + }, + "peers.dialog.title": { + "message": "Társ részletei" + }, + "networks.row.toggle": { + "message": "{name} be/ki" + }, + "networks.bulk.label": { + "message": "Összes látható erőforrás be/ki" + }, + "settings.nav.label": { + "message": "Beállítások szakaszai" + }, + "profile.switch.title": { + "message": "Váltás a(z) \"{name}\" profilra?" + }, + "profile.switch.message": { + "message": "Biztosan profilt szeretne váltani?\nAz aktuális profilja le lesz választva." + }, + "profile.switch.confirm": { + "message": "Megerősítés" + }, + "profile.deregister.title": { + "message": "\"{name}\" profil leválasztása?" + }, + "profile.deregister.message": { + "message": "Biztosan le szeretné választani ezt a profilt?\nÚjra be kell jelentkeznie a használatához." + }, + "profile.deregister.confirm": { + "message": "Leválasztás" + }, + "profile.delete.title": { + "message": "\"{name}\" profil törlése?" + }, + "profile.delete.message": { + "message": "Biztosan törölni szeretné ezt a profilt?\nEz a művelet nem vonható vissza." + }, + "profile.delete.disabledActive": { + "message": "Aktív profilok nem törölhetők. Váltson másik profilra, mielőtt törölné ezt." + }, + "profile.delete.disabledDefault": { + "message": "Az alapértelmezett profil nem törölhető." + }, + "profile.error.switchTitle": { + "message": "Profilváltás sikertelen" + }, + "profile.error.deregisterTitle": { + "message": "Leválasztás sikertelen" + }, + "profile.error.deleteTitle": { + "message": "Profil törlése sikertelen" + }, + "profile.error.createTitle": { + "message": "Profil létrehozása sikertelen" + }, + "profile.error.editTitle": { + "message": "Profil szerkesztése sikertelen" + }, + "profile.error.loadTitle": { + "message": "Profilok betöltése sikertelen" + }, + "profile.dropdown.activeProfile": { + "message": "Aktív profil" + }, + "profile.dropdown.switchProfile": { + "message": "Profilváltás" + }, + "profile.dropdown.noEmail": { + "message": "Egyéb" + }, + "profile.dropdown.addProfile": { + "message": "Profil hozzáadása" + }, + "profile.dropdown.manageProfiles": { + "message": "Profilok kezelése" + }, + "profile.dropdown.settings": { + "message": "Beállítások" + }, + "settings.profiles.section.profiles": { + "message": "Profilok" + }, + "settings.profiles.intro": { + "message": "Kezeljen több NetBird-profilt párhuzamosan, például munkahelyi és személyes fiókokat, vagy különböző felügyeleti szervereket. Lent hozzáadhat, leválaszthat vagy törölhet profilokat." + }, + "settings.profiles.addProfile": { + "message": "Profil hozzáadása" + }, + "settings.profiles.active": { + "message": "Aktív" + }, + "settings.profiles.emptyTitle": { + "message": "Nincsenek profilok" + }, + "settings.profiles.emptyDescription": { + "message": "Hozzon létre egy profilt a NetBird felügyeleti szerverhez való csatlakozáshoz." + }, + "settings.error.loadTitle": { + "message": "Beállítások betöltése sikertelen" + }, + "settings.error.saveTitle": { + "message": "Beállítások mentése sikertelen" + }, + "settings.error.debugBundleTitle": { + "message": "Hibakeresési csomag sikertelen" + }, + "settings.tabs.general": { + "message": "Általános" + }, + "settings.tabs.network": { + "message": "Hálózat" + }, + "settings.tabs.security": { + "message": "Biztonság" + }, + "settings.tabs.profiles": { + "message": "Profilok" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "Speciális" + }, + "settings.tabs.troubleshooting": { + "message": "Hibaelhárítás" + }, + "settings.tabs.about": { + "message": "Névjegy" + }, + "settings.tabs.updateAvailable": { + "message": "Frissítés elérhető" + }, + "settings.general.section.general": { + "message": "Általános" + }, + "settings.general.section.connection": { + "message": "Kapcsolat" + }, + "settings.general.connectOnStartup.label": { + "message": "Csatlakozás indításkor" + }, + "settings.general.connectOnStartup.help": { + "message": "A szolgáltatás indulásakor automatikusan kapcsolatot létesít." + }, + "settings.general.notifications.label": { + "message": "Asztali értesítések" + }, + "settings.general.notifications.help": { + "message": "Asztali értesítések megjelenítése új frissítésekről és kapcsolati eseményekről." + }, + "settings.general.autostart.label": { + "message": "NetBird UI indítása bejelentkezéskor" + }, + "settings.general.autostart.help": { + "message": "A NetBird felület automatikus indítása bejelentkezéskor. Ez csak a grafikus felületet érinti, a háttérszolgáltatást nem." + }, + "settings.general.autostart.errorTitle": { + "message": "Az automatikus indítás módosítása sikertelen" + }, + "settings.general.language.label": { + "message": "Megjelenítési nyelv" + }, + "settings.general.language.help": { + "message": "Válassza ki a NetBird felület nyelvét." + }, + "settings.general.language.search": { + "message": "Nyelv keresése…" + }, + "settings.general.language.empty": { + "message": "Nincs találat." + }, + "settings.general.management.label": { + "message": "Felügyeleti szerver" + }, + "settings.general.management.help": { + "message": "Csatlakozás a NetBird Cloudhoz vagy saját üzemeltetésű felügyeleti szerverhez. A módosítások újracsatlakozást váltanak ki." + }, + "settings.general.management.cloud": { + "message": "Felhő" + }, + "settings.general.management.selfHosted": { + "message": "Saját üzemeltetésű" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "Adjon meg egy érvényes URL-t, pl. https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "A szerver nem érhető el. Ellenőrizze az URL-t, vagy mentse el, ha biztos benne, hogy helyes." + }, + "settings.general.management.switchCloudTitle": { + "message": "Átváltás a NetBird Cloudra?" + }, + "settings.general.management.switchCloudMessage": { + "message": "Ez leválasztja a saját üzemeltetésű kiszolgálót.\nLehet, hogy újra be kell jelentkeznie." + }, + "settings.general.management.switchCloudConfirm": { + "message": "Váltás a felhőre" + }, + "settings.network.section.connectivity": { + "message": "Kapcsolódás" + }, + "settings.network.section.routingDns": { + "message": "Útválasztás és DNS" + }, + "settings.network.lazy.label": { + "message": "Igény szerinti kapcsolatok" + }, + "settings.network.lazy.help": { + "message": "Állandó kapcsolatok fenntartása helyett a NetBird igény szerint, aktivitás vagy jelzés alapján aktiválja azokat." + }, + "settings.network.monitor.label": { + "message": "Újracsatlakozás hálózatváltáskor" + }, + "settings.network.monitor.help": { + "message": "A hálózat figyelése és automatikus újracsatlakozás változások (pl. Wi-Fi-váltás, Ethernet-változás vagy alvó állapotból való visszatérés) esetén." + }, + "settings.network.dns.label": { + "message": "DNS engedélyezése" + }, + "settings.network.dns.help": { + "message": "A NetBird által kezelt DNS-beállítások alkalmazása a gazda DNS-feloldójára." + }, + "settings.network.clientRoutes.label": { + "message": "Kliens útvonalak engedélyezése" + }, + "settings.network.clientRoutes.help": { + "message": "Más Peerek útvonalainak elfogadása a hálózataik eléréséhez." + }, + "settings.network.serverRoutes.label": { + "message": "Szerver útvonalak engedélyezése" + }, + "settings.network.serverRoutes.help": { + "message": "Ennek a gazdának a helyi útvonalainak meghirdetése más Peerek számára." + }, + "settings.network.ipv6.label": { + "message": "IPv6 engedélyezése" + }, + "settings.network.ipv6.help": { + "message": "IPv6-címzés használata a NetBird overlay hálózathoz." + }, + "settings.security.section.firewall": { + "message": "Tűzfal" + }, + "settings.security.section.encryption": { + "message": "Titkosítás" + }, + "settings.security.blockInbound.label": { + "message": "Bejövő forgalom blokkolása" + }, + "settings.security.blockInbound.help": { + "message": "Visszautasítja a Peerektől érkező nem kért kapcsolatokat ezen eszközhöz és az általa irányított hálózatokhoz. A kimenő forgalmat nem érinti." + }, + "settings.security.blockLan.label": { + "message": "LAN-hozzáférés blokkolása" + }, + "settings.security.blockLan.help": { + "message": "Megakadályozza, hogy a Peerek elérjék a helyi hálózatot vagy annak eszközeit, amikor ez az eszköz irányítja a forgalmukat." + }, + "settings.security.rosenpass.label": { + "message": "Kvantumellenálló titkosítás engedélyezése" + }, + "settings.security.rosenpass.help": { + "message": "Post-kvantum kulcscsere hozzáadása Rosenpass segítségével a WireGuard® tetejére." + }, + "settings.security.rosenpassPermissive.label": { + "message": "Engedékeny mód engedélyezése" + }, + "settings.security.rosenpassPermissive.help": { + "message": "Kapcsolatok engedélyezése kvantumellenálló titkosítás nélküli Peerekkel." + }, + "settings.ssh.section.server": { + "message": "Szerver" + }, + "settings.ssh.section.capabilities": { + "message": "Képességek" + }, + "settings.ssh.section.authentication": { + "message": "Hitelesítés" + }, + "settings.ssh.server.label": { + "message": "SSH szerver engedélyezése" + }, + "settings.ssh.server.help": { + "message": "Futtassa a NetBird SSH szervert ezen a gazdán, hogy más Peerek csatlakozhassanak." + }, + "settings.ssh.root.label": { + "message": "Root bejelentkezés engedélyezése" + }, + "settings.ssh.root.help": { + "message": "A Peerek bejelentkezhetnek root felhasználóként. Tiltsa le, ha nem privilegizált fiók szükséges." + }, + "settings.ssh.sftp.label": { + "message": "SFTP engedélyezése" + }, + "settings.ssh.sftp.help": { + "message": "Fájlok biztonságos átvitele natív SFTP- vagy SCP-kliensekkel." + }, + "settings.ssh.localForward.label": { + "message": "Helyi porttovábbítás" + }, + "settings.ssh.localForward.help": { + "message": "A csatlakozó Peerek helyi portokat alagútba helyezhetnek erről a gazdáról elérhető szolgáltatásokhoz." + }, + "settings.ssh.remoteForward.label": { + "message": "Távoli porttovábbítás" + }, + "settings.ssh.remoteForward.help": { + "message": "A csatlakozó Peerek ezen a gazdán lévő portokat tehetnek elérhetővé a saját gépük számára." + }, + "settings.ssh.jwt.label": { + "message": "JWT-hitelesítés engedélyezése" + }, + "settings.ssh.jwt.help": { + "message": "Minden SSH-munkamenet ellenőrzése az IdP-vel a felhasználói identitás és audit céljából. Tiltsa le, ha csak a hálózati ACL-szabályokra kíván támaszkodni — hasznos, ha nincs elérhető IdP." + }, + "settings.ssh.jwtTtl.label": { + "message": "JWT gyorsítótár TTL" + }, + "settings.ssh.jwtTtl.help": { + "message": "Mennyi ideig őrzi meg a kliens a JWT-t, mielőtt újra kérné a kimenő SSH-kapcsolatoknál. 0 érték esetén a gyorsítótárazás kikapcsol, és minden kapcsolatnál hitelesít." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "másodperc" + }, + "settings.advanced.section.interface": { + "message": "Interfész" + }, + "settings.advanced.section.security": { + "message": "Biztonság" + }, + "settings.advanced.interfaceName.label": { + "message": "Név" + }, + "settings.advanced.interfaceName.error": { + "message": "Használjon 1–15 betűt, számot, pontot, kötőjelet vagy aláhúzást." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "„utun” után számmal kezdődjön (pl. utun100)." + }, + "settings.advanced.port.label": { + "message": "Port" + }, + "settings.advanced.port.error": { + "message": "Adjon meg egy portot {min} és {max} között." + }, + "settings.advanced.port.help": { + "message": "Ha 0-ra állítja, egy véletlenszerű szabad portot használ." + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "Adjon meg egy MTU értéket {min} és {max} között." + }, + "settings.advanced.psk.label": { + "message": "Pre-shared kulcs" + }, + "settings.advanced.psk.help": { + "message": "Opcionális WireGuard PSK további szimmetrikus titkosításhoz. Nem azonos a NetBird telepítőkulccsal. Csak olyan Peerekkel kommunikál, akik ugyanazt a pre-shared kulcsot használják." + }, + "settings.troubleshooting.section.title": { + "message": "Hibakeresési csomag" + }, + "settings.troubleshooting.anonymize.label": { + "message": "Érzékeny információk anonimizálása" + }, + "settings.troubleshooting.anonymize.help": { + "message": "Elrejti a nyilvános IP-címeket és a nem-NetBird tartományokat a naplókban." + }, + "settings.troubleshooting.systemInfo.label": { + "message": "Rendszerinformációk beillesztése" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "Tartalmazza az OS-t, a kernelt, a hálózati interfészeket és az útválasztási táblákat." + }, + "settings.troubleshooting.upload.label": { + "message": "Csomag feltöltése a NetBird szerverekre" + }, + "settings.troubleshooting.upload.help": { + "message": "Egy feltöltési kulcsot ad vissza, amelyet megoszthat a NetBird támogatással." + }, + "settings.troubleshooting.trace.label": { + "message": "Trace naplók engedélyezése" + }, + "settings.troubleshooting.trace.help": { + "message": "TRACE szintre emeli a naplózást, majd utána visszaállítja." + }, + "settings.troubleshooting.capture.label": { + "message": "Rögzítési munkamenet" + }, + "settings.troubleshooting.capture.help": { + "message": "Újra csatlakozik és vár, hogy reprodukálhassa a problémát." + }, + "settings.troubleshooting.packets.label": { + "message": "Hálózati csomagok rögzítése" + }, + "settings.troubleshooting.packets.help": { + "message": "A rögzítés ideje alatt elmenti a hálózati forgalom .pcap fájlját." + }, + "settings.troubleshooting.duration.label": { + "message": "Rögzítés időtartama" + }, + "settings.troubleshooting.duration.help": { + "message": "Mennyi ideig fusson a rögzítési munkamenet." + }, + "settings.troubleshooting.duration.suffix": { + "message": "perc" + }, + "settings.troubleshooting.create": { + "message": "Hibakeresési csomag létrehozása" + }, + "settings.troubleshooting.progress.description": { + "message": "Naplók, rendszerinformációk és kapcsolati állapot gyűjtése folyamatban. Általában néhány pillanatot vesz igénybe — tartsa nyitva ezt az ablakot a befejezésig." + }, + "settings.troubleshooting.cancelling": { + "message": "Megszakítás…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "A hibakeresési csomag feltöltése sikeres!" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "Csomag elmentve" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "Ossza meg az alábbi feltöltési kulcsot a NetBird támogatással. A helyi másolat is elmentve van az eszközén." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "A hibakeresési csomag helyileg elmentve." + }, + "settings.troubleshooting.done.copyKey": { + "message": "Kulcs másolása" + }, + "settings.troubleshooting.done.openFolder": { + "message": "Mappa megnyitása" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "Fájl helyének megnyitása" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "Feltöltés sikertelen: {reason} A csomag továbbra is el van mentve helyileg." + }, + "settings.troubleshooting.uploadFailed": { + "message": "Feltöltés sikertelen. A csomag továbbra is el van mentve helyileg." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "NetBird újracsatlakoztatása…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "Hibakeresési naplók rögzítése" + }, + "settings.troubleshooting.stage.bundling": { + "message": "Hibakeresési csomag generálása…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "Feltöltés a NetBirdhöz…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "Megszakítás…" + }, + "settings.about.client": { + "message": "NetBird Kliens v{version}" + }, + "settings.about.clientName": { + "message": "NetBird Kliens" + }, + "settings.about.development": { + "message": "[Fejlesztés]" + }, + "settings.about.gui": { + "message": "GUI v{version}" + }, + "settings.about.guiName": { + "message": "GUI" + }, + "settings.about.copyright": { + "message": "© {year} NetBird. Minden jog fenntartva." + }, + "settings.about.links.imprint": { + "message": "Impresszum" + }, + "settings.about.links.privacy": { + "message": "Adatvédelem" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "Felhasználási feltételek" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "Fórum" + }, + "settings.about.community.documentation": { + "message": "Dokumentáció" + }, + "settings.about.community.feedback": { + "message": "Visszajelzés" + }, + "update.banner.message": { + "message": "A NetBird {version} telepítésre kész." + }, + "update.banner.later": { + "message": "Később" + }, + "update.banner.installNow": { + "message": "Telepítés most" + }, + "update.card.versionAvailableDownload": { + "message": "A {version} verzió letöltésre elérhető." + }, + "update.card.versionAvailableInstall": { + "message": "A {version} verzió telepítésre elérhető." + }, + "update.card.whatsNew": { + "message": "Mi az újdonság?" + }, + "update.card.installNow": { + "message": "Telepítés most" + }, + "update.card.getInstaller": { + "message": "Letöltés" + }, + "update.card.autoCheckInterval": { + "message": "A NetBird a háttérben keres frissítéseket." + }, + "update.card.changelog": { + "message": "Változásnapló" + }, + "update.card.onLatestVersion": { + "message": "A legfrissebb verziót használja" + }, + "update.header.tooltip": { + "message": "Frissítés elérhető" + }, + "update.overlay.updatingVersion": { + "message": "NetBird frissítése a következőre: v{version}" + }, + "update.overlay.updating": { + "message": "NetBird frissítése" + }, + "update.overlay.description": { + "message": "Egy újabb verzió elérhető és települ. A NetBird automatikusan újraindul a frissítés befejeztével." + }, + "update.overlay.error.timeoutTitle": { + "message": "A frissítés túl sokáig tart" + }, + "update.overlay.error.timeoutDescription": { + "message": "A(z) {target} telepítése túl sokáig tartott, és nem fejeződött be." + }, + "update.overlay.error.canceledTitle": { + "message": "A frissítés megszakítva" + }, + "update.overlay.error.canceledDescription": { + "message": "A(z) {target} frissítését megszakították a befejezés előtt." + }, + "update.overlay.error.failTitle": { + "message": "A frissítés nem telepíthető" + }, + "update.overlay.error.failDescription": { + "message": "A(z) {target} nem volt telepíthető." + }, + "update.overlay.error.unknownMessage": { + "message": "ismeretlen hiba" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "az új verzió" + }, + "update.error.loadStateTitle": { + "message": "Frissítési állapot betöltése sikertelen" + }, + "update.error.triggerTitle": { + "message": "Frissítés indítása sikertelen" + }, + "update.page.versionLine": { + "message": "Kliens frissítése erre: {version}." + }, + "update.page.versionLineGeneric": { + "message": "Kliens frissítése." + }, + "update.page.outdated": { + "message": "Az Ön kliensverziója régebbi, mint a Managementben beállított automatikus frissítési verzió." + }, + "update.page.status.running": { + "message": "Frissítés" + }, + "update.page.status.timeout": { + "message": "A frissítés időtúllépés miatt megszakadt. Kérjük, próbálja újra." + }, + "update.page.status.canceled": { + "message": "Frissítés megszakítva." + }, + "update.page.status.failed": { + "message": "Frissítés sikertelen: {message}" + }, + "update.page.status.unknownError": { + "message": "ismeretlen frissítési hiba" + }, + "update.page.failedTitle": { + "message": "Frissítés sikertelen" + }, + "update.page.timeoutMessage": { + "message": "Frissítés időtúllépés." + }, + "update.page.dontClose": { + "message": "Kérjük, ne zárja be ezt az ablakot." + }, + "update.page.updating": { + "message": "Frissítés…" + }, + "update.page.complete": { + "message": "Frissítés kész" + }, + "update.page.failed": { + "message": "Frissítés sikertelen" + }, + "window.title.settings": { + "message": "Beállítások" + }, + "window.title.signIn": { + "message": "Bejelentkezés" + }, + "window.title.sessionExpiration": { + "message": "Munkamenet lejár" + }, + "window.title.updating": { + "message": "Frissítés" + }, + "window.title.welcome": { + "message": "Üdvözli a NetBird" + }, + "window.title.error": { + "message": "Hiba" + }, + "welcome.title": { + "message": "Keresse a NetBirdöt a tálcán" + }, + "welcome.description": { + "message": "A NetBird a tálcán fut. Kattintson az ikonra a csatlakozáshoz, profilváltáshoz vagy a beállítások megnyitásához." + }, + "welcome.continue": { + "message": "Folytatás" + }, + "welcome.back": { + "message": "Vissza" + }, + "welcome.management.title": { + "message": "NetBird beállítása" + }, + "welcome.management.description": { + "message": "Kattintson a Folytatás gombra a kezdéshez, vagy válassza a „Saját üzemeltetésű” lehetőséget, ha saját NetBird-szervere van." + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud" + }, + "welcome.management.cloud.description": { + "message": "Használja az általunk üzemeltetett szolgáltatást. Nincs szükség beállításra." + }, + "welcome.management.selfHosted.title": { + "message": "Saját üzemeltetésű" + }, + "welcome.management.selfHosted.description": { + "message": "Csatlakozás a saját felügyeleti szerveréhez." + }, + "welcome.management.urlLabel": { + "message": "Felügyeleti szerver URL" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "Adjon meg egy érvényes URL-t, pl. https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "A szerver nem érhető el. Ellenőrizze az URL-t vagy a hálózatot, majd folytassa, ha biztos benne, hogy helyes." + }, + "welcome.management.checking": { + "message": "Ellenőrzés…" + }, + "browserLogin.title": { + "message": "Folytassa a böngészőben a bejelentkezés befejezéséhez" + }, + "browserLogin.notSeeing": { + "message": "Nem látja a böngésző fülét?" + }, + "browserLogin.tryAgain": { + "message": "Próbálja újra" + }, + "browserLogin.openFailedTitle": { + "message": "A böngésző megnyitása sikertelen" + }, + "sessionExpiration.title": { + "message": "A munkamenet hamarosan lejár" + }, + "sessionExpiration.titleLater": { + "message": "A munkamenete lejár" + }, + "sessionExpiration.description": { + "message": "Az eszköz hamarosan lecsatlakozik. Megújításhoz böngészős bejelentkezés kell." + }, + "sessionExpiration.descriptionLater": { + "message": "Egy böngészős bejelentkezés a hálózaton tartja az eszközt." + }, + "sessionExpiration.stay": { + "message": "Munkamenet megújítása" + }, + "sessionExpiration.authenticate": { + "message": "Bejelentkezés" + }, + "sessionExpiration.logout": { + "message": "Kijelentkezés" + }, + "sessionExpiration.expired": { + "message": "Munkamenet lejárt" + }, + "sessionExpiration.expiredDescription": { + "message": "Eszköz lecsatlakozott. Hitelesítés böngészős bejelentkezéssel az újracsatlakozáshoz." + }, + "sessionExpiration.close": { + "message": "Bezárás" + }, + "sessionExpiration.extendFailedTitle": { + "message": "A munkamenet meghosszabbítása sikertelen" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "Kijelentkezés sikertelen" + }, + "peers.search.placeholder": { + "message": "Keresés név vagy IP alapján" + }, + "peers.filter.all": { + "message": "Összes" + }, + "peers.filter.online": { + "message": "Online" + }, + "peers.filter.offline": { + "message": "Offline" + }, + "peers.empty.title": { + "message": "Nincs elérhető Peer" + }, + "peers.empty.description": { + "message": "Önnek vagy nincsenek elérhető Peerei, vagy nincs hozzáférése egyikhez sem." + }, + "peers.details.domain": { + "message": "Domain" + }, + "peers.details.netbirdIp": { + "message": "NetBird IP" + }, + "peers.details.netbirdIpv6": { + "message": "NetBird IPv6" + }, + "peers.details.publicKey": { + "message": "Nyilvános kulcs" + }, + "peers.details.connection": { + "message": "Kapcsolat" + }, + "peers.details.latency": { + "message": "Késleltetés" + }, + "peers.details.lastHandshake": { + "message": "Utolsó Handshake" + }, + "peers.details.statusSince": { + "message": "Utolsó kapcsolati frissítés" + }, + "peers.details.bytes": { + "message": "Bájtok" + }, + "peers.details.bytesSent": { + "message": "Küldve" + }, + "peers.details.bytesReceived": { + "message": "Fogadva" + }, + "peers.details.localIce": { + "message": "Helyi ICE" + }, + "peers.details.remoteIce": { + "message": "Távoli ICE" + }, + "peers.details.never": { + "message": "Soha" + }, + "peers.details.justNow": { + "message": "Épp most" + }, + "peers.details.refresh": { + "message": "Frissítés" + }, + "peers.status.connected": { + "message": "Csatlakozva" + }, + "peers.status.connecting": { + "message": "Csatlakozás" + }, + "peers.status.disconnected": { + "message": "Lecsatlakozva" + }, + "peers.details.relayAddress": { + "message": "Relay" + }, + "peers.details.networks": { + "message": "Erőforrások" + }, + "peers.details.relayed": { + "message": "Relayed" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "Rosenpass engedélyezve" + }, + "networks.search.placeholder": { + "message": "Keresés hálózat vagy domain alapján" + }, + "networks.filter.all": { + "message": "Összes" + }, + "networks.filter.active": { + "message": "Aktív" + }, + "networks.filter.overlapping": { + "message": "Átfedő" + }, + "networks.empty.title": { + "message": "Nincs elérhető erőforrás" + }, + "networks.empty.description": { + "message": "Önnek vagy nincsenek elérhető hálózati erőforrásai vagy nincs hozzáférése egyikhez sem." + }, + "networks.selected": { + "message": "Kiválasztva" + }, + "networks.unselected": { + "message": "Nincs kiválasztva" + }, + "networks.ips.heading": { + "message": "Feloldott IP-címek" + }, + "networks.bulk.selectionCount": { + "message": "{selected} / {total} aktív" + }, + "networks.bulk.enableAll": { + "message": "Összes engedélyezése" + }, + "networks.bulk.disableAll": { + "message": "Összes letiltása" + }, + "exitNodes.search.placeholder": { + "message": "Keresés az Exit Node-ok között" + }, + "exitNodes.none": { + "message": "Egyik sem" + }, + "exitNodes.empty.title": { + "message": "Nincs elérhető Exit Node" + }, + "exitNodes.empty.description": { + "message": "Ehhez a Peerhez nem osztottak meg Exit Node-okat." + }, + "exitNodes.card.title": { + "message": "Exit Node" + }, + "exitNodes.card.statusActive": { + "message": "Aktív" + }, + "exitNodes.card.statusInactive": { + "message": "Inaktív" + }, + "exitNodes.dropdown.noneTitle": { + "message": "Egyik sem" + }, + "exitNodes.dropdown.noneDescription": { + "message": "Közvetlen kapcsolat Exit Node nélkül" + }, + "quickActions.connect": { + "message": "Csatlakozás" + }, + "quickActions.disconnect": { + "message": "Bontás" + }, + "daemon.unavailable.title": { + "message": "A NetBird szolgáltatás nem fut" + }, + "daemon.unavailable.description": { + "message": "Az alkalmazás automatikusan újracsatlakozik, amint a szolgáltatás újra elérhető." + }, + "daemon.unavailable.docsLink": { + "message": "Dokumentáció" + }, + "daemon.outdated.title": { + "message": "A NetBird szolgáltatás elavult" + }, + "daemon.outdated.description": { + "message": "Frissítsd a NetBird szolgáltatást az alkalmazás használatához." + }, + "error.jwt_clock_skew": { + "message": "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": { + "message": "A bejelentkezési token lejárt. Kérjük, jelentkezzen be újra." + }, + "error.jwt_signature_invalid": { + "message": "A bejelentkezés sikertelen: a token aláírása érvénytelen. Kérjük, lépjen kapcsolatba a rendszergazdával." + }, + "error.session_expired": { + "message": "A munkamenet lejárt. Kérjük, jelentkezzen be újra." + }, + "error.invalid_setup_key": { + "message": "A telepítőkulcs hiányzik vagy érvénytelen." + }, + "error.permission_denied": { + "message": "A szerver elutasította a bejelentkezést." + }, + "error.daemon_unreachable": { + "message": "A NetBird szolgáltatás nem válaszol. Kérjük, ellenőrizze, hogy fut-e a szolgáltatás." + }, + "error.unknown": { + "message": "A művelet meghiúsult." + } +} diff --git a/client/ui/i18n/locales/it/common.json b/client/ui/i18n/locales/it/common.json new file mode 100644 index 000000000..3224be972 --- /dev/null +++ b/client/ui/i18n/locales/it/common.json @@ -0,0 +1,1331 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "Disconnesso" + }, + "tray.status.daemonUnavailable": { + "message": "Non in esecuzione" + }, + "tray.status.error": { + "message": "Errore" + }, + "tray.status.connected": { + "message": "Connesso" + }, + "tray.status.connecting": { + "message": "Connessione" + }, + "tray.status.needsLogin": { + "message": "Accesso richiesto" + }, + "tray.status.loginFailed": { + "message": "Accesso non riuscito" + }, + "tray.status.sessionExpired": { + "message": "Sessione scaduta" + }, + "tray.session.expiresIn": { + "message": "La sessione scade tra {remaining}" + }, + "tray.session.unit.lessThanMinute": { + "message": "meno di un minuto" + }, + "tray.session.unit.minute": { + "message": "1 minuto" + }, + "tray.session.unit.minutes": { + "message": "{count} minuti" + }, + "tray.session.unit.hour": { + "message": "1 ora" + }, + "tray.session.unit.hours": { + "message": "{count} ore" + }, + "tray.session.unit.day": { + "message": "1 giorno" + }, + "tray.session.unit.days": { + "message": "{count} giorni" + }, + "tray.menu.open": { + "message": "Apri NetBird" + }, + "tray.menu.connect": { + "message": "Connetti" + }, + "tray.menu.disconnect": { + "message": "Disconnetti" + }, + "tray.menu.exitNode": { + "message": "Nodo di uscita" + }, + "tray.menu.networks": { + "message": "Risorse" + }, + "tray.menu.profiles": { + "message": "Profili" + }, + "tray.menu.manageProfiles": { + "message": "Gestisci profili" + }, + "tray.menu.settings": { + "message": "Impostazioni..." + }, + "tray.menu.debugBundle": { + "message": "Crea pacchetto di debug" + }, + "tray.menu.about": { + "message": "Guida e supporto" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "Documentazione" + }, + "tray.menu.troubleshoot": { + "message": "Risoluzione dei problemi" + }, + "tray.menu.downloadLatest": { + "message": "Scarica l'ultima versione" + }, + "tray.menu.installVersion": { + "message": "Installa la versione {version}" + }, + "tray.menu.guiVersion": { + "message": "GUI: {version}" + }, + "tray.menu.daemonVersion": { + "message": "Daemon: {version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "Esci da NetBird" + }, + "notify.daemonOutdated.title": { + "message": "Il servizio NetBird è obsoleto" + }, + "notify.daemonOutdated.body": { + "message": "Aggiorna il servizio NetBird per usare questa app." + }, + "notify.update.title": { + "message": "Aggiornamento NetBird disponibile" + }, + "notify.update.body": { + "message": "NetBird {version} è disponibile." + }, + "notify.update.enforcedSuffix": { + "message": " L'amministratore richiede questo aggiornamento." + }, + "notify.error.title": { + "message": "Errore" + }, + "notify.error.connect": { + "message": "Connessione non riuscita" + }, + "notify.error.disconnect": { + "message": "Disconnessione non riuscita" + }, + "notify.error.switchProfile": { + "message": "Impossibile passare a {profile}" + }, + "notify.error.exitNode": { + "message": "Impossibile aggiornare il nodo di uscita {name}" + }, + "notify.sessionExpired.title": { + "message": "Sessione NetBird scaduta" + }, + "notify.sessionExpired.body": { + "message": "La sessione NetBird è scaduta. Effettui di nuovo l'accesso." + }, + "notify.sessionWarning.title": { + "message": "La sessione sta per scadere" + }, + "notify.sessionWarning.body": { + "message": "La sessione NetBird scade tra {remaining}. Clicchi su Rinnova ora per rinnovarla." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "La sessione NetBird sta per scadere. Clicchi su Rinnova ora per rinnovarla." + }, + "notify.sessionWarning.extend": { + "message": "Rinnova ora" + }, + "notify.sessionWarning.dismiss": { + "message": "Ignora" + }, + "notify.sessionWarning.failed": { + "message": "Impossibile rinnovare la sessione NetBird" + }, + "notify.sessionWarning.successTitle": { + "message": "Sessione NetBird rinnovata" + }, + "notify.sessionWarning.successBody": { + "message": "La sessione è stata rinnovata." + }, + "notify.sessionDeadlineRejected.title": { + "message": "Scadenza sessione rifiutata" + }, + "notify.sessionDeadlineRejected.body": { + "message": "Il server ha inviato una scadenza di sessione non valida. Effettui di nuovo l'accesso." + }, + "notify.mdm.policyApplied.title": { + "message": "Impostazioni NetBird aggiornate" + }, + "notify.mdm.policyApplied.body": { + "message": "La configurazione di NetBird è stata aggiornata dalla policy IT." + }, + "common.cancel": { + "message": "Annulla" + }, + "common.save": { + "message": "Salva" + }, + "common.saveChanges": { + "message": "Salva modifiche" + }, + "common.saving": { + "message": "Salvataggio…" + }, + "common.close": { + "message": "Chiudi" + }, + "common.copy": { + "message": "Copia" + }, + "common.togglePasswordVisibility": { + "message": "Mostra/nascondi password" + }, + "common.increase": { + "message": "Aumenta" + }, + "common.decrease": { + "message": "Diminuisci" + }, + "common.delete": { + "message": "Elimina" + }, + "common.create": { + "message": "Crea" + }, + "common.add": { + "message": "Aggiungi" + }, + "common.remove": { + "message": "Rimuovi" + }, + "common.refresh": { + "message": "Aggiorna" + }, + "common.loading": { + "message": "Caricamento…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "Nessun risultato trovato" + }, + "common.noResults.description": { + "message": "Non è stato trovato alcun risultato. Provi un altro termine di ricerca o modifichi i filtri." + }, + "notConnected.title": { + "message": "Disconnesso" + }, + "notConnected.description": { + "message": "Si connetta prima a NetBird per visualizzare informazioni dettagliate su peer, risorse di rete e nodi di uscita." + }, + "connect.status.disconnected": { + "message": "Disconnesso" + }, + "connect.status.connecting": { + "message": "Connessione..." + }, + "connect.status.connected": { + "message": "Connesso" + }, + "connect.status.disconnecting": { + "message": "Disconnessione..." + }, + "connect.status.daemonUnavailable": { + "message": "Daemon non disponibile" + }, + "connect.status.loginRequired": { + "message": "Accesso richiesto" + }, + "connect.error.loginTitle": { + "message": "Accesso non riuscito" + }, + "connect.error.connectTitle": { + "message": "Connessione non riuscita" + }, + "connect.error.disconnectTitle": { + "message": "Disconnessione non riuscita" + }, + "nav.peers.title": { + "message": "Peer" + }, + "nav.peers.description": { + "message": "{connected} di {total} connessi" + }, + "nav.resources.title": { + "message": "Risorse" + }, + "nav.resources.description": { + "message": "{active} di {total} attive" + }, + "nav.exitNode.title": { + "message": "Nodi di uscita" + }, + "nav.exitNode.none": { + "message": "Non attivo" + }, + "nav.exitNode.using": { + "message": "Tramite {name}" + }, + "header.openSettings": { + "message": "Apri impostazioni" + }, + "header.togglePanel": { + "message": "Mostra/nascondi pannello laterale" + }, + "profile.selector.loading": { + "message": "Caricamento..." + }, + "profile.selector.noProfile": { + "message": "Nessun profilo" + }, + "profile.selector.searchPlaceholder": { + "message": "Cerca profilo per nome..." + }, + "profile.selector.emptyTitle": { + "message": "Nessun profilo trovato" + }, + "profile.selector.emptyDescription": { + "message": "Provi un altro termine di ricerca o crei un nuovo profilo." + }, + "profile.selector.newProfile": { + "message": "Nuovo profilo" + }, + "profile.selector.moreOptions": { + "message": "Altre opzioni" + }, + "profile.selector.deregister": { + "message": "Annulla registrazione" + }, + "profile.selector.delete": { + "message": "Elimina" + }, + "profile.selector.switchTo": { + "message": "Passa a questo profilo" + }, + "profile.selector.edit": { + "message": "Modifica" + }, + "profile.edit.title": { + "message": "Modifica profilo" + }, + "profile.edit.submit": { + "message": "Salva modifiche" + }, + "profile.dialog.title": { + "message": "Inserisci il nome del profilo" + }, + "profile.dialog.nameLabel": { + "message": "Nome del profilo" + }, + "profile.dialog.description": { + "message": "Imposti un nome facilmente riconoscibile per il profilo." + }, + "profile.dialog.placeholder": { + "message": "es. lavoro" + }, + "profile.dialog.submit": { + "message": "Aggiungi profilo" + }, + "profile.dialog.required": { + "message": "Inserisca un nome per il profilo, es. lavoro, casa" + }, + "profile.dialog.managementHelp": { + "message": "Usi NetBird Cloud o il suo server." + }, + "profile.dialog.urlUnreachable": { + "message": "Impossibile raggiungere questo server. Controlli l'URL, oppure aggiunga comunque il profilo se è certo che sia corretto." + }, + "header.menu.settings": { + "message": "Impostazioni..." + }, + "header.menu.defaultView": { + "message": "Vista predefinita" + }, + "header.menu.advancedView": { + "message": "Vista avanzata" + }, + "header.menu.updateAvailable": { + "message": "Aggiornamento disponibile" + }, + "header.menu.open": { + "message": "Apri menu" + }, + "header.profile.switch": { + "message": "Cambia profilo" + }, + "connect.toggle.label": { + "message": "Attiva/disattiva connessione NetBird" + }, + "connect.localIp.label": { + "message": "Indirizzi IP locali" + }, + "common.search": { + "message": "Cerca" + }, + "common.filter": { + "message": "Filtra" + }, + "exitNodes.dropdown.trigger": { + "message": "Seleziona nodo di uscita" + }, + "peers.row.label": { + "message": "Apri dettagli di {name}, {status}" + }, + "peers.dialog.title": { + "message": "Dettagli peer" + }, + "networks.row.toggle": { + "message": "Attiva/disattiva {name}" + }, + "networks.bulk.label": { + "message": "Attiva/disattiva tutte le risorse visibili" + }, + "settings.nav.label": { + "message": "Sezioni delle impostazioni" + }, + "profile.switch.title": { + "message": "Passare al profilo «{name}»?" + }, + "profile.switch.message": { + "message": "Vuole davvero cambiare profilo?\nIl profilo attuale verrà disconnesso." + }, + "profile.switch.confirm": { + "message": "Conferma" + }, + "profile.deregister.title": { + "message": "Annullare la registrazione del profilo «{name}»?" + }, + "profile.deregister.message": { + "message": "Vuole davvero annullare la registrazione di questo profilo?\nDovrà accedere di nuovo per usarlo." + }, + "profile.deregister.confirm": { + "message": "Annulla registrazione" + }, + "profile.delete.title": { + "message": "Eliminare il profilo «{name}»?" + }, + "profile.delete.message": { + "message": "Vuole davvero eliminare questo profilo?\nQuesta azione non può essere annullata." + }, + "profile.delete.disabledActive": { + "message": "I profili attivi non possono essere eliminati. Passi a un altro profilo prima di eliminare questo." + }, + "profile.delete.disabledDefault": { + "message": "Il profilo predefinito non può essere eliminato." + }, + "profile.error.switchTitle": { + "message": "Cambio profilo non riuscito" + }, + "profile.error.deregisterTitle": { + "message": "Annullamento registrazione non riuscito" + }, + "profile.error.deleteTitle": { + "message": "Eliminazione profilo non riuscita" + }, + "profile.error.createTitle": { + "message": "Creazione profilo non riuscita" + }, + "profile.error.editTitle": { + "message": "Modifica del profilo non riuscita" + }, + "profile.error.loadTitle": { + "message": "Caricamento profili non riuscito" + }, + "profile.dropdown.activeProfile": { + "message": "Profilo attivo" + }, + "profile.dropdown.switchProfile": { + "message": "Cambia profilo" + }, + "profile.dropdown.noEmail": { + "message": "Altro" + }, + "profile.dropdown.addProfile": { + "message": "Aggiungi profilo" + }, + "profile.dropdown.manageProfiles": { + "message": "Gestisci profili" + }, + "profile.dropdown.settings": { + "message": "Impostazioni" + }, + "settings.profiles.section.profiles": { + "message": "Profili" + }, + "settings.profiles.intro": { + "message": "Mantenga affiancate identità NetBird separate, ad esempio account di lavoro e personali, oppure server di gestione diversi. Aggiunga, annulli la registrazione o elimini i profili qui sotto." + }, + "settings.profiles.addProfile": { + "message": "Aggiungi profilo" + }, + "settings.profiles.active": { + "message": "Attivo" + }, + "settings.profiles.emptyTitle": { + "message": "Nessun profilo" + }, + "settings.profiles.emptyDescription": { + "message": "Crei un profilo per connettersi a un server di gestione NetBird." + }, + "settings.error.loadTitle": { + "message": "Caricamento impostazioni non riuscito" + }, + "settings.error.saveTitle": { + "message": "Salvataggio impostazioni non riuscito" + }, + "settings.error.debugBundleTitle": { + "message": "Pacchetto di debug non riuscito" + }, + "settings.tabs.general": { + "message": "Generale" + }, + "settings.tabs.network": { + "message": "Rete" + }, + "settings.tabs.security": { + "message": "Sicurezza" + }, + "settings.tabs.profiles": { + "message": "Profili" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "Avanzate" + }, + "settings.tabs.troubleshooting": { + "message": "Risoluzione problemi" + }, + "settings.tabs.about": { + "message": "Informazioni" + }, + "settings.tabs.updateAvailable": { + "message": "Aggiornamento disponibile" + }, + "settings.general.section.general": { + "message": "Generale" + }, + "settings.general.section.connection": { + "message": "Connessione" + }, + "settings.general.connectOnStartup.label": { + "message": "Connetti all'avvio" + }, + "settings.general.connectOnStartup.help": { + "message": "Stabilisce automaticamente una connessione all'avvio del servizio." + }, + "settings.general.notifications.label": { + "message": "Notifiche desktop" + }, + "settings.general.notifications.help": { + "message": "Mostra notifiche desktop per nuovi aggiornamenti ed eventi di connessione." + }, + "settings.general.autostart.label": { + "message": "Avvia l'interfaccia NetBird all'accesso" + }, + "settings.general.autostart.help": { + "message": "Avvia automaticamente l'interfaccia NetBird quando effettua l'accesso. Riguarda solo l'interfaccia grafica, non il servizio in background." + }, + "settings.general.autostart.errorTitle": { + "message": "Modifica avvio automatico non riuscita" + }, + "settings.general.language.label": { + "message": "Lingua dell'interfaccia" + }, + "settings.general.language.help": { + "message": "Scelga la lingua dell'interfaccia NetBird." + }, + "settings.general.language.search": { + "message": "Cerca lingua…" + }, + "settings.general.language.empty": { + "message": "Nessuna lingua corrisponde." + }, + "settings.general.management.label": { + "message": "Server di gestione" + }, + "settings.general.management.help": { + "message": "Si connetta a NetBird Cloud o al suo server di gestione self-hosted. Le modifiche riconnettono il client." + }, + "settings.general.management.cloud": { + "message": "Cloud" + }, + "settings.general.management.selfHosted": { + "message": "Self-hosted" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "Inserisca un URL valido, es. https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "Impossibile raggiungere questo server. Controlli l'URL, oppure salvi comunque se è certo che sia corretto." + }, + "settings.general.management.switchCloudTitle": { + "message": "Passare a NetBird Cloud?" + }, + "settings.general.management.switchCloudMessage": { + "message": "Questo disconnette il suo server self-hosted.\nPotrebbe dover accedere di nuovo." + }, + "settings.general.management.switchCloudConfirm": { + "message": "Passa a Cloud" + }, + "settings.network.section.connectivity": { + "message": "Connettività" + }, + "settings.network.section.routingDns": { + "message": "Routing e DNS" + }, + "settings.network.lazy.label": { + "message": "Connessioni lazy" + }, + "settings.network.lazy.help": { + "message": "Invece di mantenere connessioni sempre attive, NetBird le attiva su richiesta in base all'attività o al signaling." + }, + "settings.network.monitor.label": { + "message": "Riconnetti al cambio di rete" + }, + "settings.network.monitor.help": { + "message": "Monitora la rete e si riconnette automaticamente ai cambiamenti, come il passaggio di Wi-Fi, le modifiche Ethernet o la ripresa dalla sospensione." + }, + "settings.network.dns.label": { + "message": "Abilita DNS" + }, + "settings.network.dns.help": { + "message": "Applica le impostazioni DNS gestite da NetBird al resolver dell'host." + }, + "settings.network.clientRoutes.label": { + "message": "Abilita route client" + }, + "settings.network.clientRoutes.help": { + "message": "Accetta le route da altri peer per raggiungere le loro reti." + }, + "settings.network.serverRoutes.label": { + "message": "Abilita route server" + }, + "settings.network.serverRoutes.help": { + "message": "Annuncia le route locali di questo host agli altri peer." + }, + "settings.network.ipv6.label": { + "message": "Abilita IPv6" + }, + "settings.network.ipv6.help": { + "message": "Usa l'indirizzamento IPv6 per la rete overlay NetBird." + }, + "settings.security.section.firewall": { + "message": "Firewall" + }, + "settings.security.section.encryption": { + "message": "Crittografia" + }, + "settings.security.blockInbound.label": { + "message": "Blocca traffico in entrata" + }, + "settings.security.blockInbound.help": { + "message": "Rifiuta le connessioni non richieste dai peer verso questo dispositivo e le reti che instrada. Il traffico in uscita non è interessato." + }, + "settings.security.blockLan.label": { + "message": "Blocca accesso alla LAN" + }, + "settings.security.blockLan.help": { + "message": "Impedisce ai peer di raggiungere la sua rete locale o i suoi dispositivi quando questo dispositivo instrada il loro traffico." + }, + "settings.security.rosenpass.label": { + "message": "Abilita resistenza quantistica" + }, + "settings.security.rosenpass.help": { + "message": "Aggiunge uno scambio di chiavi post-quantistico tramite Rosenpass sopra WireGuard®." + }, + "settings.security.rosenpassPermissive.label": { + "message": "Abilita modalità permissiva" + }, + "settings.security.rosenpassPermissive.help": { + "message": "Consente le connessioni con peer privi del supporto alla resistenza quantistica." + }, + "settings.ssh.section.server": { + "message": "Server" + }, + "settings.ssh.section.capabilities": { + "message": "Funzionalità" + }, + "settings.ssh.section.authentication": { + "message": "Autenticazione" + }, + "settings.ssh.server.label": { + "message": "Abilita server SSH" + }, + "settings.ssh.server.help": { + "message": "Esegue il server SSH di NetBird su questo host in modo che altri peer possano connettersi." + }, + "settings.ssh.root.label": { + "message": "Consenti accesso come root" + }, + "settings.ssh.root.help": { + "message": "Permette ai peer di accedere come utente root. Disabiliti per richiedere un account senza privilegi." + }, + "settings.ssh.sftp.label": { + "message": "Consenti SFTP" + }, + "settings.ssh.sftp.help": { + "message": "Trasferisca file in modo sicuro usando client SFTP o SCP nativi." + }, + "settings.ssh.localForward.label": { + "message": "Inoltro porte locale" + }, + "settings.ssh.localForward.help": { + "message": "Permette ai peer in connessione di inoltrare porte locali verso servizi raggiungibili da questo host." + }, + "settings.ssh.remoteForward.label": { + "message": "Inoltro porte remoto" + }, + "settings.ssh.remoteForward.help": { + "message": "Permette ai peer in connessione di esporre porte di questo host verso la loro macchina." + }, + "settings.ssh.jwt.label": { + "message": "Abilita autenticazione JWT" + }, + "settings.ssh.jwt.help": { + "message": "Verifica ogni sessione SSH con il suo IdP per l'identità utente e l'audit. Disabiliti per basarsi solo sulle policy ACL di rete, utile quando non è disponibile un IdP." + }, + "settings.ssh.jwtTtl.label": { + "message": "TTL cache JWT" + }, + "settings.ssh.jwtTtl.help": { + "message": "Per quanto tempo questo client memorizza un JWT prima di richiederlo di nuovo sulle connessioni SSH in uscita. Imposti 0 per disabilitare la cache e autenticarsi a ogni connessione." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "sec." + }, + "settings.advanced.section.interface": { + "message": "Interfaccia" + }, + "settings.advanced.section.security": { + "message": "Sicurezza" + }, + "settings.advanced.interfaceName.label": { + "message": "Nome" + }, + "settings.advanced.interfaceName.error": { + "message": "Usi da 1 a 15 lettere, cifre, punti, trattini o trattini bassi." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "Deve iniziare con \"utun\" seguito da un numero (es. utun100)." + }, + "settings.advanced.port.label": { + "message": "Porta" + }, + "settings.advanced.port.error": { + "message": "Inserisca una porta compresa tra {min} e {max}." + }, + "settings.advanced.port.help": { + "message": "Se impostata su 0, verrà usata una porta libera casuale." + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "Inserisca un valore MTU compreso tra {min} e {max}." + }, + "settings.advanced.psk.label": { + "message": "Chiave pre-condivisa" + }, + "settings.advanced.psk.help": { + "message": "PSK WireGuard opzionale per una crittografia simmetrica aggiuntiva. Non è la stessa cosa di una chiave di configurazione NetBird. Comunicherà solo con i peer che usano la stessa chiave pre-condivisa." + }, + "settings.troubleshooting.section.title": { + "message": "Pacchetto di debug" + }, + "settings.troubleshooting.anonymize.label": { + "message": "Anonimizza informazioni sensibili" + }, + "settings.troubleshooting.anonymize.help": { + "message": "Nasconde gli indirizzi IP pubblici e i domini non NetBird dai log." + }, + "settings.troubleshooting.systemInfo.label": { + "message": "Includi informazioni di sistema" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "Include OS, kernel, interfacce di rete e tabelle di routing." + }, + "settings.troubleshooting.upload.label": { + "message": "Carica il pacchetto sui server NetBird" + }, + "settings.troubleshooting.upload.help": { + "message": "Restituisce una chiave di caricamento da condividere con il supporto NetBird." + }, + "settings.troubleshooting.trace.label": { + "message": "Abilita log di traccia" + }, + "settings.troubleshooting.trace.help": { + "message": "Aumenta il livello di log a TRACE e lo ripristina al termine." + }, + "settings.troubleshooting.capture.label": { + "message": "Sessione di acquisizione" + }, + "settings.troubleshooting.capture.help": { + "message": "Si riconnette e attende per consentirle di riprodurre il problema." + }, + "settings.troubleshooting.packets.label": { + "message": "Acquisisci pacchetti di rete" + }, + "settings.troubleshooting.packets.help": { + "message": "Salva un .pcap del traffico di rete durante la sessione di acquisizione." + }, + "settings.troubleshooting.duration.label": { + "message": "Durata acquisizione" + }, + "settings.troubleshooting.duration.help": { + "message": "Per quanto tempo viene eseguita la sessione di acquisizione." + }, + "settings.troubleshooting.duration.suffix": { + "message": "min." + }, + "settings.troubleshooting.create": { + "message": "Crea pacchetto" + }, + "settings.troubleshooting.progress.description": { + "message": "Raccolta di log, dettagli di sistema e stato della connessione. Di solito richiede un momento: tenga aperta questa finestra fino al completamento." + }, + "settings.troubleshooting.cancelling": { + "message": "Annullamento…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "Pacchetto di debug caricato con successo!" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "Pacchetto salvato" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "Condivida la chiave di caricamento qui sotto con il supporto NetBird. Una copia locale è stata salvata anche sul suo dispositivo." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "Il pacchetto di debug è stato salvato localmente." + }, + "settings.troubleshooting.done.copyKey": { + "message": "Copia chiave" + }, + "settings.troubleshooting.done.openFolder": { + "message": "Apri cartella" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "Apri posizione file" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "Caricamento non riuscito: {reason} Il pacchetto è comunque salvato localmente." + }, + "settings.troubleshooting.uploadFailed": { + "message": "Caricamento non riuscito. Il pacchetto è comunque salvato localmente." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "Riconnessione di NetBird…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "Acquisizione log di debug" + }, + "settings.troubleshooting.stage.bundling": { + "message": "Generazione del pacchetto di debug…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "Caricamento su NetBird…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "Annullamento…" + }, + "settings.about.client": { + "message": "NetBird Client v{version}" + }, + "settings.about.clientName": { + "message": "NetBird Client" + }, + "settings.about.development": { + "message": "[Development]" + }, + "settings.about.gui": { + "message": "GUI v{version}" + }, + "settings.about.guiName": { + "message": "GUI" + }, + "settings.about.copyright": { + "message": "© {year} NetBird. Tutti i diritti riservati." + }, + "settings.about.links.imprint": { + "message": "Note legali" + }, + "settings.about.links.privacy": { + "message": "Privacy" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "Termini di servizio" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "Forum" + }, + "settings.about.community.documentation": { + "message": "Documentazione" + }, + "settings.about.community.feedback": { + "message": "Feedback" + }, + "update.banner.message": { + "message": "NetBird {version} è pronto per l'installazione." + }, + "update.banner.later": { + "message": "Più tardi" + }, + "update.banner.installNow": { + "message": "Installa ora" + }, + "update.card.versionAvailableDownload": { + "message": "La versione {version} è disponibile per il download." + }, + "update.card.versionAvailableInstall": { + "message": "La versione {version} è disponibile per l'installazione." + }, + "update.card.whatsNew": { + "message": "Novità?" + }, + "update.card.installNow": { + "message": "Installa ora" + }, + "update.card.getInstaller": { + "message": "Scarica" + }, + "update.card.autoCheckInterval": { + "message": "NetBird verifica gli aggiornamenti in background." + }, + "update.card.changelog": { + "message": "Changelog" + }, + "update.card.onLatestVersion": { + "message": "Sta usando l'ultima versione" + }, + "update.header.tooltip": { + "message": "Aggiornamento disponibile" + }, + "update.overlay.updatingVersion": { + "message": "Aggiornamento di NetBird alla v{version}" + }, + "update.overlay.updating": { + "message": "Aggiornamento di NetBird" + }, + "update.overlay.description": { + "message": "È disponibile una versione più recente ed è in corso l'installazione. NetBird si riavvierà automaticamente al termine dell'aggiornamento." + }, + "update.overlay.error.timeoutTitle": { + "message": "L'aggiornamento richiede troppo tempo" + }, + "update.overlay.error.timeoutDescription": { + "message": "L'installazione di {target} ha richiesto troppo tempo e non è stata completata." + }, + "update.overlay.error.canceledTitle": { + "message": "Aggiornamento interrotto" + }, + "update.overlay.error.canceledDescription": { + "message": "L'aggiornamento a {target} è stato annullato prima del completamento." + }, + "update.overlay.error.failTitle": { + "message": "Impossibile installare l'aggiornamento" + }, + "update.overlay.error.failDescription": { + "message": "Impossibile installare {target}." + }, + "update.overlay.error.unknownMessage": { + "message": "errore sconosciuto" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "la nuova versione" + }, + "update.error.loadStateTitle": { + "message": "Caricamento stato aggiornamento non riuscito" + }, + "update.error.triggerTitle": { + "message": "Avvio aggiornamento non riuscito" + }, + "update.page.versionLine": { + "message": "Aggiornamento del client a: {version}." + }, + "update.page.versionLineGeneric": { + "message": "Aggiornamento del client." + }, + "update.page.outdated": { + "message": "La versione del client è precedente alla versione di aggiornamento automatico impostata in Management." + }, + "update.page.status.running": { + "message": "Aggiornamento in corso" + }, + "update.page.status.timeout": { + "message": "Aggiornamento scaduto. Riprovi." + }, + "update.page.status.canceled": { + "message": "Aggiornamento annullato." + }, + "update.page.status.failed": { + "message": "Aggiornamento non riuscito: {message}" + }, + "update.page.status.unknownError": { + "message": "errore di aggiornamento sconosciuto" + }, + "update.page.failedTitle": { + "message": "Aggiornamento non riuscito" + }, + "update.page.timeoutMessage": { + "message": "Aggiornamento scaduto." + }, + "update.page.dontClose": { + "message": "Non chiuda questa finestra." + }, + "update.page.updating": { + "message": "Aggiornamento…" + }, + "update.page.complete": { + "message": "Aggiornamento completato" + }, + "update.page.failed": { + "message": "Aggiornamento non riuscito" + }, + "window.title.settings": { + "message": "Impostazioni" + }, + "window.title.signIn": { + "message": "Accesso" + }, + "window.title.sessionExpiration": { + "message": "Sessione in scadenza" + }, + "window.title.updating": { + "message": "Aggiornamento" + }, + "window.title.welcome": { + "message": "Benvenuto in NetBird" + }, + "window.title.error": { + "message": "Errore" + }, + "welcome.title": { + "message": "Cerchi NetBird nella tray" + }, + "welcome.description": { + "message": "NetBird risiede nella tray. Clicchi sull'icona per connettersi, cambiare profilo o aprire le impostazioni." + }, + "welcome.continue": { + "message": "Continua" + }, + "welcome.back": { + "message": "Indietro" + }, + "welcome.management.title": { + "message": "Configura NetBird" + }, + "welcome.management.description": { + "message": "Clicchi su Continua per iniziare, oppure scelga Self-hosted se dispone di un proprio server NetBird." + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud" + }, + "welcome.management.cloud.description": { + "message": "Usi il nostro servizio gestito. Nessuna configurazione necessaria." + }, + "welcome.management.selfHosted.title": { + "message": "Self-hosted" + }, + "welcome.management.selfHosted.description": { + "message": "Si connetta al suo server di gestione." + }, + "welcome.management.urlLabel": { + "message": "URL del server di gestione" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "Inserisca un URL valido, es. https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "Impossibile raggiungere questo server. Controlli l'URL o la sua rete, poi prosegua se è certo che sia corretto." + }, + "welcome.management.checking": { + "message": "Verifica…" + }, + "browserLogin.title": { + "message": "Continui nel browser per completare l'accesso" + }, + "browserLogin.notSeeing": { + "message": "Non vede la scheda del browser?" + }, + "browserLogin.tryAgain": { + "message": "Riprova" + }, + "browserLogin.openFailedTitle": { + "message": "Apertura browser non riuscita" + }, + "sessionExpiration.title": { + "message": "La sessione sta per scadere" + }, + "sessionExpiration.titleLater": { + "message": "La sessione scadrà" + }, + "sessionExpiration.description": { + "message": "Questo dispositivo verrà disconnesso a breve. Rinnovi con un accesso dal browser." + }, + "sessionExpiration.descriptionLater": { + "message": "Un accesso dal browser mantiene questo dispositivo connesso alla sua rete." + }, + "sessionExpiration.stay": { + "message": "Rinnova sessione" + }, + "sessionExpiration.authenticate": { + "message": "Autenticati" + }, + "sessionExpiration.logout": { + "message": "Esci" + }, + "sessionExpiration.expired": { + "message": "Sessione scaduta" + }, + "sessionExpiration.expiredDescription": { + "message": "Dispositivo disconnesso. Si autentichi con un accesso dal browser per riconnettersi." + }, + "sessionExpiration.close": { + "message": "Chiudi" + }, + "sessionExpiration.extendFailedTitle": { + "message": "Rinnovo sessione non riuscito" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "Disconnessione non riuscita" + }, + "peers.search.placeholder": { + "message": "Cerca per nome o IP" + }, + "peers.filter.all": { + "message": "Tutti" + }, + "peers.filter.online": { + "message": "Online" + }, + "peers.filter.offline": { + "message": "Offline" + }, + "peers.empty.title": { + "message": "Nessun peer disponibile" + }, + "peers.empty.description": { + "message": "Non ha peer disponibili oppure non ha accesso a nessuno di essi." + }, + "peers.details.domain": { + "message": "Dominio" + }, + "peers.details.netbirdIp": { + "message": "IP NetBird" + }, + "peers.details.netbirdIpv6": { + "message": "IPv6 NetBird" + }, + "peers.details.publicKey": { + "message": "Chiave pubblica" + }, + "peers.details.connection": { + "message": "Connessione" + }, + "peers.details.latency": { + "message": "Latenza" + }, + "peers.details.lastHandshake": { + "message": "Ultimo handshake" + }, + "peers.details.statusSince": { + "message": "Ultimo aggiornamento connessione" + }, + "peers.details.bytes": { + "message": "Byte" + }, + "peers.details.bytesSent": { + "message": "Inviati" + }, + "peers.details.bytesReceived": { + "message": "Ricevuti" + }, + "peers.details.localIce": { + "message": "ICE locale" + }, + "peers.details.remoteIce": { + "message": "ICE remoto" + }, + "peers.details.never": { + "message": "Mai" + }, + "peers.details.justNow": { + "message": "Proprio ora" + }, + "peers.details.refresh": { + "message": "Aggiorna" + }, + "peers.status.connected": { + "message": "Connesso" + }, + "peers.status.connecting": { + "message": "Connessione" + }, + "peers.status.disconnected": { + "message": "Disconnesso" + }, + "peers.details.relayAddress": { + "message": "Relay" + }, + "peers.details.networks": { + "message": "Risorse" + }, + "peers.details.relayed": { + "message": "Tramite relay" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "Rosenpass abilitato" + }, + "networks.search.placeholder": { + "message": "Cerca per rete o dominio" + }, + "networks.filter.all": { + "message": "Tutte" + }, + "networks.filter.active": { + "message": "Attive" + }, + "networks.filter.overlapping": { + "message": "Sovrapposte" + }, + "networks.empty.title": { + "message": "Nessuna risorsa disponibile" + }, + "networks.empty.description": { + "message": "Non ha risorse di rete disponibili oppure non ha accesso a nessuna di esse." + }, + "networks.selected": { + "message": "Selezionata" + }, + "networks.unselected": { + "message": "Non selezionata" + }, + "networks.ips.heading": { + "message": "IP risolti" + }, + "networks.bulk.selectionCount": { + "message": "{selected} di {total} attive" + }, + "networks.bulk.enableAll": { + "message": "Abilita tutte" + }, + "networks.bulk.disableAll": { + "message": "Disabilita tutte" + }, + "exitNodes.search.placeholder": { + "message": "Cerca nodi di uscita" + }, + "exitNodes.none": { + "message": "Nessuno" + }, + "exitNodes.empty.title": { + "message": "Nessun nodo di uscita disponibile" + }, + "exitNodes.empty.description": { + "message": "Nessun nodo di uscita è stato condiviso con questo peer." + }, + "exitNodes.card.title": { + "message": "Nodo di uscita" + }, + "exitNodes.card.statusActive": { + "message": "Attivo" + }, + "exitNodes.card.statusInactive": { + "message": "Inattivo" + }, + "exitNodes.dropdown.noneTitle": { + "message": "Nessuno" + }, + "exitNodes.dropdown.noneDescription": { + "message": "Connessione diretta senza nodo di uscita" + }, + "quickActions.connect": { + "message": "Connetti" + }, + "quickActions.disconnect": { + "message": "Disconnetti" + }, + "daemon.unavailable.title": { + "message": "Il servizio NetBird non è in esecuzione" + }, + "daemon.unavailable.description": { + "message": "L'app si riconnetterà automaticamente non appena il servizio sarà in esecuzione." + }, + "daemon.unavailable.docsLink": { + "message": "Documentazione" + }, + "daemon.outdated.title": { + "message": "Il servizio NetBird è obsoleto" + }, + "daemon.outdated.description": { + "message": "Aggiorna il servizio NetBird per usare questa app." + }, + "error.jwt_clock_skew": { + "message": "Accesso non riuscito: l'orologio di questo dispositivo non è sincronizzato con il server. Sincronizzi l'orologio di sistema e riprovi." + }, + "error.jwt_expired": { + "message": "Il token di accesso è scaduto. Effettui di nuovo l'accesso." + }, + "error.jwt_signature_invalid": { + "message": "Accesso non riuscito: la firma del token non è valida. Contatti il suo amministratore." + }, + "error.session_expired": { + "message": "La sessione è scaduta. Effettui di nuovo l'accesso." + }, + "error.invalid_setup_key": { + "message": "La chiave di configurazione è mancante o non valida." + }, + "error.permission_denied": { + "message": "L'accesso è stato rifiutato dal server." + }, + "error.daemon_unreachable": { + "message": "Il daemon NetBird non risponde. Verifichi che il servizio sia in esecuzione." + }, + "error.unknown": { + "message": "Operazione non riuscita." + } +} diff --git a/client/ui/i18n/locales/pt/common.json b/client/ui/i18n/locales/pt/common.json new file mode 100644 index 000000000..75b977204 --- /dev/null +++ b/client/ui/i18n/locales/pt/common.json @@ -0,0 +1,1331 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "Desconectado" + }, + "tray.status.daemonUnavailable": { + "message": "Não está em execução" + }, + "tray.status.error": { + "message": "Erro" + }, + "tray.status.connected": { + "message": "Conectado" + }, + "tray.status.connecting": { + "message": "Conectando" + }, + "tray.status.needsLogin": { + "message": "Login necessário" + }, + "tray.status.loginFailed": { + "message": "Falha no login" + }, + "tray.status.sessionExpired": { + "message": "Sessão expirada" + }, + "tray.session.expiresIn": { + "message": "A sessão expira em {remaining}" + }, + "tray.session.unit.lessThanMinute": { + "message": "menos de um minuto" + }, + "tray.session.unit.minute": { + "message": "1 minuto" + }, + "tray.session.unit.minutes": { + "message": "{count} minutos" + }, + "tray.session.unit.hour": { + "message": "1 hora" + }, + "tray.session.unit.hours": { + "message": "{count} horas" + }, + "tray.session.unit.day": { + "message": "1 dia" + }, + "tray.session.unit.days": { + "message": "{count} dias" + }, + "tray.menu.open": { + "message": "Abrir o NetBird" + }, + "tray.menu.connect": { + "message": "Conectar" + }, + "tray.menu.disconnect": { + "message": "Desconectar" + }, + "tray.menu.exitNode": { + "message": "Nó de saída" + }, + "tray.menu.networks": { + "message": "Recursos" + }, + "tray.menu.profiles": { + "message": "Perfis" + }, + "tray.menu.manageProfiles": { + "message": "Gerenciar perfis" + }, + "tray.menu.settings": { + "message": "Configurações..." + }, + "tray.menu.debugBundle": { + "message": "Criar pacote de depuração" + }, + "tray.menu.about": { + "message": "Ajuda e suporte" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "Documentação" + }, + "tray.menu.troubleshoot": { + "message": "Solução de problemas" + }, + "tray.menu.downloadLatest": { + "message": "Baixar a versão mais recente" + }, + "tray.menu.installVersion": { + "message": "Instalar a versão {version}" + }, + "tray.menu.guiVersion": { + "message": "GUI: {version}" + }, + "tray.menu.daemonVersion": { + "message": "Daemon: {version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "Sair do NetBird" + }, + "notify.daemonOutdated.title": { + "message": "O serviço NetBird está desatualizado" + }, + "notify.daemonOutdated.body": { + "message": "Atualize o serviço NetBird para usar este aplicativo." + }, + "notify.update.title": { + "message": "Atualização do NetBird disponível" + }, + "notify.update.body": { + "message": "O NetBird {version} está disponível." + }, + "notify.update.enforcedSuffix": { + "message": " O seu administrador exige esta atualização." + }, + "notify.error.title": { + "message": "Erro" + }, + "notify.error.connect": { + "message": "Falha ao conectar" + }, + "notify.error.disconnect": { + "message": "Falha ao desconectar" + }, + "notify.error.switchProfile": { + "message": "Falha ao alternar para {profile}" + }, + "notify.error.exitNode": { + "message": "Falha ao atualizar o nó de saída {name}" + }, + "notify.sessionExpired.title": { + "message": "Sessão do NetBird expirada" + }, + "notify.sessionExpired.body": { + "message": "A sua sessão do NetBird expirou. Faça login novamente." + }, + "notify.sessionWarning.title": { + "message": "A sessão expira em breve" + }, + "notify.sessionWarning.body": { + "message": "A sua sessão do NetBird expira em {remaining}. Clique em Renovar agora para renová-la." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "A sua sessão do NetBird está prestes a expirar. Clique em Renovar agora para renová-la." + }, + "notify.sessionWarning.extend": { + "message": "Renovar agora" + }, + "notify.sessionWarning.dismiss": { + "message": "Dispensar" + }, + "notify.sessionWarning.failed": { + "message": "Falha ao renovar a sessão do NetBird" + }, + "notify.sessionWarning.successTitle": { + "message": "Sessão do NetBird renovada" + }, + "notify.sessionWarning.successBody": { + "message": "A sua sessão foi renovada." + }, + "notify.sessionDeadlineRejected.title": { + "message": "Prazo da sessão rejeitado" + }, + "notify.sessionDeadlineRejected.body": { + "message": "O servidor enviou um prazo de sessão inválido. Faça login novamente." + }, + "notify.mdm.policyApplied.title": { + "message": "Definições do NetBird atualizadas" + }, + "notify.mdm.policyApplied.body": { + "message": "A sua configuração do NetBird foi atualizada pela política de TI." + }, + "common.cancel": { + "message": "Cancelar" + }, + "common.save": { + "message": "Salvar" + }, + "common.saveChanges": { + "message": "Salvar alterações" + }, + "common.saving": { + "message": "Salvando…" + }, + "common.close": { + "message": "Fechar" + }, + "common.copy": { + "message": "Copiar" + }, + "common.togglePasswordVisibility": { + "message": "Alternar visibilidade da senha" + }, + "common.increase": { + "message": "Aumentar" + }, + "common.decrease": { + "message": "Diminuir" + }, + "common.delete": { + "message": "Excluir" + }, + "common.create": { + "message": "Criar" + }, + "common.add": { + "message": "Adicionar" + }, + "common.remove": { + "message": "Remover" + }, + "common.refresh": { + "message": "Atualizar" + }, + "common.loading": { + "message": "Carregando…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "Nenhum resultado encontrado" + }, + "common.noResults.description": { + "message": "Não encontramos nenhum resultado. Tente um termo de busca diferente ou altere os filtros." + }, + "notConnected.title": { + "message": "Desconectado" + }, + "notConnected.description": { + "message": "Conecte-se ao NetBird primeiro para ver informações detalhadas sobre seus peers, recursos de rede e nós de saída." + }, + "connect.status.disconnected": { + "message": "Desconectado" + }, + "connect.status.connecting": { + "message": "Conectando..." + }, + "connect.status.connected": { + "message": "Conectado" + }, + "connect.status.disconnecting": { + "message": "Desconectando..." + }, + "connect.status.daemonUnavailable": { + "message": "Daemon indisponível" + }, + "connect.status.loginRequired": { + "message": "Login necessário" + }, + "connect.error.loginTitle": { + "message": "Falha no login" + }, + "connect.error.connectTitle": { + "message": "Falha ao conectar" + }, + "connect.error.disconnectTitle": { + "message": "Falha ao desconectar" + }, + "nav.peers.title": { + "message": "Peers" + }, + "nav.peers.description": { + "message": "{connected} de {total} conectados" + }, + "nav.resources.title": { + "message": "Recursos" + }, + "nav.resources.description": { + "message": "{active} de {total} ativos" + }, + "nav.exitNode.title": { + "message": "Nós de saída" + }, + "nav.exitNode.none": { + "message": "Inativo" + }, + "nav.exitNode.using": { + "message": "Via {name}" + }, + "header.openSettings": { + "message": "Abrir configurações" + }, + "header.togglePanel": { + "message": "Alternar painel lateral" + }, + "profile.selector.loading": { + "message": "Carregando..." + }, + "profile.selector.noProfile": { + "message": "Nenhum perfil" + }, + "profile.selector.searchPlaceholder": { + "message": "Buscar perfil por nome..." + }, + "profile.selector.emptyTitle": { + "message": "Nenhum perfil encontrado" + }, + "profile.selector.emptyDescription": { + "message": "Tente um termo de busca diferente ou crie um novo perfil." + }, + "profile.selector.newProfile": { + "message": "Novo perfil" + }, + "profile.selector.moreOptions": { + "message": "Mais opções" + }, + "profile.selector.deregister": { + "message": "Cancelar registro" + }, + "profile.selector.delete": { + "message": "Excluir" + }, + "profile.selector.switchTo": { + "message": "Alternar para este perfil" + }, + "profile.selector.edit": { + "message": "Editar" + }, + "profile.edit.title": { + "message": "Editar perfil" + }, + "profile.edit.submit": { + "message": "Salvar alterações" + }, + "profile.dialog.title": { + "message": "Insira o nome do perfil" + }, + "profile.dialog.nameLabel": { + "message": "Nome do perfil" + }, + "profile.dialog.description": { + "message": "Defina um nome fácil de identificar para o seu perfil." + }, + "profile.dialog.placeholder": { + "message": "ex.: trabalho" + }, + "profile.dialog.submit": { + "message": "Adicionar perfil" + }, + "profile.dialog.required": { + "message": "Insira um nome de perfil, ex.: trabalho, casa" + }, + "profile.dialog.managementHelp": { + "message": "Use o NetBird Cloud ou seu próprio servidor." + }, + "profile.dialog.urlUnreachable": { + "message": "Não foi possível acessar este servidor. Verifique a URL ou adicione o perfil mesmo assim se tiver certeza de que está correta." + }, + "header.menu.settings": { + "message": "Configurações..." + }, + "header.menu.defaultView": { + "message": "Visualização padrão" + }, + "header.menu.advancedView": { + "message": "Visualização avançada" + }, + "header.menu.updateAvailable": { + "message": "Atualização disponível" + }, + "header.menu.open": { + "message": "Abrir menu" + }, + "header.profile.switch": { + "message": "Trocar perfil" + }, + "connect.toggle.label": { + "message": "Alternar conexão NetBird" + }, + "connect.localIp.label": { + "message": "Endereços IP locais" + }, + "common.search": { + "message": "Pesquisar" + }, + "common.filter": { + "message": "Filtrar" + }, + "exitNodes.dropdown.trigger": { + "message": "Selecionar nó de saída" + }, + "peers.row.label": { + "message": "Abrir detalhes de {name}, {status}" + }, + "peers.dialog.title": { + "message": "Detalhes do par" + }, + "networks.row.toggle": { + "message": "Alternar {name}" + }, + "networks.bulk.label": { + "message": "Alternar todos os recursos visíveis" + }, + "settings.nav.label": { + "message": "Seções das configurações" + }, + "profile.switch.title": { + "message": "Alternar perfil para \"{name}\"?" + }, + "profile.switch.message": { + "message": "Tem certeza de que deseja alternar de perfil?\nO seu perfil atual será desconectado." + }, + "profile.switch.confirm": { + "message": "Confirmar" + }, + "profile.deregister.title": { + "message": "Cancelar registro do perfil \"{name}\"?" + }, + "profile.deregister.message": { + "message": "Tem certeza de que deseja cancelar o registro deste perfil?\nVocê precisará fazer login novamente para usá-lo." + }, + "profile.deregister.confirm": { + "message": "Cancelar registro" + }, + "profile.delete.title": { + "message": "Excluir o perfil \"{name}\"?" + }, + "profile.delete.message": { + "message": "Tem certeza de que deseja excluir este perfil?\nEsta ação não pode ser desfeita." + }, + "profile.delete.disabledActive": { + "message": "Perfis ativos não podem ser excluídos. Alterne para outro antes de excluir este perfil." + }, + "profile.delete.disabledDefault": { + "message": "O perfil padrão não pode ser excluído." + }, + "profile.error.switchTitle": { + "message": "Falha ao alternar de perfil" + }, + "profile.error.deregisterTitle": { + "message": "Falha ao cancelar registro do perfil" + }, + "profile.error.deleteTitle": { + "message": "Falha ao excluir o perfil" + }, + "profile.error.createTitle": { + "message": "Falha ao criar o perfil" + }, + "profile.error.editTitle": { + "message": "Falha ao editar o perfil" + }, + "profile.error.loadTitle": { + "message": "Falha ao carregar os perfis" + }, + "profile.dropdown.activeProfile": { + "message": "Perfil ativo" + }, + "profile.dropdown.switchProfile": { + "message": "Alternar perfil" + }, + "profile.dropdown.noEmail": { + "message": "Outro" + }, + "profile.dropdown.addProfile": { + "message": "Adicionar perfil" + }, + "profile.dropdown.manageProfiles": { + "message": "Gerenciar perfis" + }, + "profile.dropdown.settings": { + "message": "Configurações" + }, + "settings.profiles.section.profiles": { + "message": "Perfis" + }, + "settings.profiles.intro": { + "message": "Mantenha identidades separadas do NetBird lado a lado, por exemplo contas de trabalho e pessoais, ou diferentes servidores de gerenciamento. Adicione, cancele o registro ou exclua perfis abaixo." + }, + "settings.profiles.addProfile": { + "message": "Adicionar perfil" + }, + "settings.profiles.active": { + "message": "Ativo" + }, + "settings.profiles.emptyTitle": { + "message": "Nenhum perfil" + }, + "settings.profiles.emptyDescription": { + "message": "Crie um perfil para conectar a um servidor de gerenciamento do NetBird." + }, + "settings.error.loadTitle": { + "message": "Falha ao carregar as configurações" + }, + "settings.error.saveTitle": { + "message": "Falha ao salvar as configurações" + }, + "settings.error.debugBundleTitle": { + "message": "Falha no pacote de depuração" + }, + "settings.tabs.general": { + "message": "Geral" + }, + "settings.tabs.network": { + "message": "Rede" + }, + "settings.tabs.security": { + "message": "Segurança" + }, + "settings.tabs.profiles": { + "message": "Perfis" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "Avançado" + }, + "settings.tabs.troubleshooting": { + "message": "Solução de problemas" + }, + "settings.tabs.about": { + "message": "Sobre" + }, + "settings.tabs.updateAvailable": { + "message": "Atualização disponível" + }, + "settings.general.section.general": { + "message": "Geral" + }, + "settings.general.section.connection": { + "message": "Conexão" + }, + "settings.general.connectOnStartup.label": { + "message": "Conectar ao iniciar" + }, + "settings.general.connectOnStartup.help": { + "message": "Estabelecer uma conexão automaticamente quando o serviço iniciar." + }, + "settings.general.notifications.label": { + "message": "Notificações na área de trabalho" + }, + "settings.general.notifications.help": { + "message": "Mostrar notificações na área de trabalho para novas atualizações e eventos de conexão." + }, + "settings.general.autostart.label": { + "message": "Iniciar a interface do NetBird ao fazer login" + }, + "settings.general.autostart.help": { + "message": "Iniciar a interface do NetBird automaticamente quando você fizer login. Isto afeta apenas a interface gráfica, não o serviço em segundo plano." + }, + "settings.general.autostart.errorTitle": { + "message": "Falha ao alterar o início automático" + }, + "settings.general.language.label": { + "message": "Idioma de exibição" + }, + "settings.general.language.help": { + "message": "Escolha o idioma da interface do NetBird." + }, + "settings.general.language.search": { + "message": "Buscar idioma…" + }, + "settings.general.language.empty": { + "message": "Nenhum idioma corresponde." + }, + "settings.general.management.label": { + "message": "Servidor de gerenciamento" + }, + "settings.general.management.help": { + "message": "Conecte ao NetBird Cloud ou ao seu próprio servidor de gerenciamento auto-hospedado. As alterações reconectarão o cliente." + }, + "settings.general.management.cloud": { + "message": "Cloud" + }, + "settings.general.management.selfHosted": { + "message": "Auto-hospedado" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "Insira uma URL válida, ex.: https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "Não foi possível acessar este servidor. Verifique a URL ou salve mesmo assim se tiver certeza de que está correta." + }, + "settings.general.management.switchCloudTitle": { + "message": "Alternar para o NetBird Cloud?" + }, + "settings.general.management.switchCloudMessage": { + "message": "Isto desconecta o seu servidor auto-hospedado.\nVocê pode precisar fazer login novamente." + }, + "settings.general.management.switchCloudConfirm": { + "message": "Alternar para o Cloud" + }, + "settings.network.section.connectivity": { + "message": "Conectividade" + }, + "settings.network.section.routingDns": { + "message": "Roteamento e DNS" + }, + "settings.network.lazy.label": { + "message": "Conexões sob demanda" + }, + "settings.network.lazy.help": { + "message": "Em vez de manter conexões sempre ativas, o NetBird as ativa sob demanda com base na atividade ou na sinalização." + }, + "settings.network.monitor.label": { + "message": "Reconectar ao mudar de rede" + }, + "settings.network.monitor.help": { + "message": "Monitora a rede e reconecta automaticamente diante de mudanças como troca de Wi-Fi, alterações na Ethernet ou retorno do modo de suspensão." + }, + "settings.network.dns.label": { + "message": "Ativar DNS" + }, + "settings.network.dns.help": { + "message": "Aplicar as configurações de DNS gerenciadas pelo NetBird ao resolvedor do host." + }, + "settings.network.clientRoutes.label": { + "message": "Ativar rotas de cliente" + }, + "settings.network.clientRoutes.help": { + "message": "Aceitar rotas de outros peers para alcançar as redes deles." + }, + "settings.network.serverRoutes.label": { + "message": "Ativar rotas de servidor" + }, + "settings.network.serverRoutes.help": { + "message": "Anunciar as rotas locais deste host para outros peers." + }, + "settings.network.ipv6.label": { + "message": "Ativar IPv6" + }, + "settings.network.ipv6.help": { + "message": "Usar endereçamento IPv6 para a rede de sobreposição do NetBird." + }, + "settings.security.section.firewall": { + "message": "Firewall" + }, + "settings.security.section.encryption": { + "message": "Criptografia" + }, + "settings.security.blockInbound.label": { + "message": "Bloquear tráfego de entrada" + }, + "settings.security.blockInbound.help": { + "message": "Rejeitar conexões não solicitadas de peers para este dispositivo e quaisquer redes que ele roteie. O tráfego de saída não é afetado." + }, + "settings.security.blockLan.label": { + "message": "Bloquear acesso à LAN" + }, + "settings.security.blockLan.help": { + "message": "Impedir que peers alcancem a sua rede local ou os dispositivos dela quando este dispositivo roteia o tráfego deles." + }, + "settings.security.rosenpass.label": { + "message": "Ativar resistência quântica" + }, + "settings.security.rosenpass.help": { + "message": "Adicionar uma troca de chaves pós-quântica via Rosenpass sobre o WireGuard®." + }, + "settings.security.rosenpassPermissive.label": { + "message": "Ativar modo permissivo" + }, + "settings.security.rosenpassPermissive.help": { + "message": "Permitir conexões com peers sem suporte a resistência quântica." + }, + "settings.ssh.section.server": { + "message": "Servidor" + }, + "settings.ssh.section.capabilities": { + "message": "Recursos" + }, + "settings.ssh.section.authentication": { + "message": "Autenticação" + }, + "settings.ssh.server.label": { + "message": "Ativar servidor SSH" + }, + "settings.ssh.server.help": { + "message": "Executar o servidor SSH do NetBird neste host para que outros peers possam conectar a ele." + }, + "settings.ssh.root.label": { + "message": "Permitir login como root" + }, + "settings.ssh.root.help": { + "message": "Permitir que peers façam login como usuário root. Desative para exigir uma conta sem privilégios." + }, + "settings.ssh.sftp.label": { + "message": "Permitir SFTP" + }, + "settings.ssh.sftp.help": { + "message": "Transferir arquivos com segurança usando clientes SFTP ou SCP nativos." + }, + "settings.ssh.localForward.label": { + "message": "Encaminhamento de porta local" + }, + "settings.ssh.localForward.help": { + "message": "Permitir que peers conectados encaminhem portas locais para serviços acessíveis a partir deste host." + }, + "settings.ssh.remoteForward.label": { + "message": "Encaminhamento de porta remota" + }, + "settings.ssh.remoteForward.help": { + "message": "Permitir que peers conectados exponham portas deste host de volta para a própria máquina deles." + }, + "settings.ssh.jwt.label": { + "message": "Ativar autenticação JWT" + }, + "settings.ssh.jwt.help": { + "message": "Verificar cada sessão SSH no seu IdP para identidade do usuário e auditoria. Desative para depender apenas das políticas de ACL da rede, útil quando nenhum IdP está disponível." + }, + "settings.ssh.jwtTtl.label": { + "message": "TTL do cache de JWT" + }, + "settings.ssh.jwtTtl.help": { + "message": "Por quanto tempo este cliente mantém um JWT em cache antes de solicitar novamente em conexões SSH de saída. Defina como 0 para desativar o cache e autenticar em cada conexão." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "s" + }, + "settings.advanced.section.interface": { + "message": "Interface" + }, + "settings.advanced.section.security": { + "message": "Segurança" + }, + "settings.advanced.interfaceName.label": { + "message": "Nome" + }, + "settings.advanced.interfaceName.error": { + "message": "Use de 1 a 15 letras, dígitos, pontos, hifens ou sublinhados." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "Deve começar com \"utun\" seguido de um número (ex.: utun100)." + }, + "settings.advanced.port.label": { + "message": "Porta" + }, + "settings.advanced.port.error": { + "message": "Insira uma porta entre {min} e {max}." + }, + "settings.advanced.port.help": { + "message": "Se definida como 0, uma porta livre aleatória será usada." + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "Insira um valor de MTU entre {min} e {max}." + }, + "settings.advanced.psk.label": { + "message": "Chave pré-compartilhada" + }, + "settings.advanced.psk.help": { + "message": "PSK opcional do WireGuard para criptografia simétrica adicional. Não é o mesmo que uma chave de configuração do NetBird. Você só se comunicará com peers que usem a mesma chave pré-compartilhada." + }, + "settings.troubleshooting.section.title": { + "message": "Pacote de depuração" + }, + "settings.troubleshooting.anonymize.label": { + "message": "Anonimizar informações sensíveis" + }, + "settings.troubleshooting.anonymize.help": { + "message": "Oculta endereços IP públicos e domínios que não são do NetBird nos logs." + }, + "settings.troubleshooting.systemInfo.label": { + "message": "Incluir informações do sistema" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "Incluir o OS, o kernel, as interfaces de rede e as tabelas de roteamento." + }, + "settings.troubleshooting.upload.label": { + "message": "Enviar pacote aos servidores do NetBird" + }, + "settings.troubleshooting.upload.help": { + "message": "Retorna uma chave de upload para compartilhar com o suporte do NetBird." + }, + "settings.troubleshooting.trace.label": { + "message": "Habilitar logs de trace" + }, + "settings.troubleshooting.trace.help": { + "message": "Eleva o nível de log para TRACE e o restaura em seguida." + }, + "settings.troubleshooting.capture.label": { + "message": "Sessão de captura" + }, + "settings.troubleshooting.capture.help": { + "message": "Reconecta e aguarda para que você possa reproduzir o problema." + }, + "settings.troubleshooting.packets.label": { + "message": "Capturar pacotes de rede" + }, + "settings.troubleshooting.packets.help": { + "message": "Salva um .pcap do tráfego de rede durante a sessão de captura." + }, + "settings.troubleshooting.duration.label": { + "message": "Duração da captura" + }, + "settings.troubleshooting.duration.help": { + "message": "Por quanto tempo a sessão de captura é executada." + }, + "settings.troubleshooting.duration.suffix": { + "message": "min" + }, + "settings.troubleshooting.create": { + "message": "Criar pacote" + }, + "settings.troubleshooting.progress.description": { + "message": "Coletando logs, detalhes do sistema e estado da conexão. Isto costuma levar um instante — mantenha esta janela aberta até concluir." + }, + "settings.troubleshooting.cancelling": { + "message": "Cancelando…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "Pacote de depuração enviado com sucesso!" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "Pacote salvo" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "Compartilhe a chave de upload abaixo com o suporte do NetBird. Uma cópia local também foi salva no seu dispositivo." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "O seu pacote de depuração foi salvo localmente." + }, + "settings.troubleshooting.done.copyKey": { + "message": "Copiar chave" + }, + "settings.troubleshooting.done.openFolder": { + "message": "Abrir pasta" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "Abrir local do arquivo" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "Falha no upload: {reason} O pacote continua salvo localmente." + }, + "settings.troubleshooting.uploadFailed": { + "message": "Falha no upload. O pacote continua salvo localmente." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "Reconectando o NetBird…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "Capturando logs de depuração" + }, + "settings.troubleshooting.stage.bundling": { + "message": "Gerando o pacote de depuração…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "Enviando para o NetBird…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "Cancelando…" + }, + "settings.about.client": { + "message": "NetBird Client v{version}" + }, + "settings.about.clientName": { + "message": "NetBird Client" + }, + "settings.about.development": { + "message": "[Development]" + }, + "settings.about.gui": { + "message": "GUI v{version}" + }, + "settings.about.guiName": { + "message": "GUI" + }, + "settings.about.copyright": { + "message": "© {year} NetBird. Todos os direitos reservados." + }, + "settings.about.links.imprint": { + "message": "Identificação legal" + }, + "settings.about.links.privacy": { + "message": "Privacidade" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "Termos de serviço" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "Fórum" + }, + "settings.about.community.documentation": { + "message": "Documentação" + }, + "settings.about.community.feedback": { + "message": "Feedback" + }, + "update.banner.message": { + "message": "O NetBird {version} está pronto para instalar." + }, + "update.banner.later": { + "message": "Mais tarde" + }, + "update.banner.installNow": { + "message": "Instalar agora" + }, + "update.card.versionAvailableDownload": { + "message": "A versão {version} está disponível para download." + }, + "update.card.versionAvailableInstall": { + "message": "A versão {version} está disponível para instalação." + }, + "update.card.whatsNew": { + "message": "Novidades?" + }, + "update.card.installNow": { + "message": "Instalar agora" + }, + "update.card.getInstaller": { + "message": "Baixar" + }, + "update.card.autoCheckInterval": { + "message": "O NetBird verifica atualizações em segundo plano." + }, + "update.card.changelog": { + "message": "Registro de alterações" + }, + "update.card.onLatestVersion": { + "message": "Você está na versão mais recente" + }, + "update.header.tooltip": { + "message": "Atualização disponível" + }, + "update.overlay.updatingVersion": { + "message": "Atualizando o NetBird para a v{version}" + }, + "update.overlay.updating": { + "message": "Atualizando o NetBird" + }, + "update.overlay.description": { + "message": "Uma versão mais recente está disponível e sendo instalada. O NetBird reiniciará automaticamente quando a atualização terminar." + }, + "update.overlay.error.timeoutTitle": { + "message": "A atualização está demorando demais" + }, + "update.overlay.error.timeoutDescription": { + "message": "A instalação de {target} demorou demais e não foi concluída." + }, + "update.overlay.error.canceledTitle": { + "message": "A atualização foi interrompida" + }, + "update.overlay.error.canceledDescription": { + "message": "A atualização para {target} foi cancelada antes de terminar." + }, + "update.overlay.error.failTitle": { + "message": "Não foi possível instalar a atualização" + }, + "update.overlay.error.failDescription": { + "message": "Não foi possível instalar {target}." + }, + "update.overlay.error.unknownMessage": { + "message": "erro desconhecido" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "a nova versão" + }, + "update.error.loadStateTitle": { + "message": "Falha ao carregar o estado da atualização" + }, + "update.error.triggerTitle": { + "message": "Falha ao iniciar a atualização" + }, + "update.page.versionLine": { + "message": "Atualizando o cliente para: {version}." + }, + "update.page.versionLineGeneric": { + "message": "Atualizando o cliente." + }, + "update.page.outdated": { + "message": "A versão do seu cliente é anterior à versão de atualização automática definida no Management." + }, + "update.page.status.running": { + "message": "Atualizando" + }, + "update.page.status.timeout": { + "message": "A atualização expirou. Tente novamente." + }, + "update.page.status.canceled": { + "message": "Atualização cancelada." + }, + "update.page.status.failed": { + "message": "Falha na atualização: {message}" + }, + "update.page.status.unknownError": { + "message": "erro de atualização desconhecido" + }, + "update.page.failedTitle": { + "message": "Falha na atualização" + }, + "update.page.timeoutMessage": { + "message": "A atualização expirou." + }, + "update.page.dontClose": { + "message": "Não feche esta janela." + }, + "update.page.updating": { + "message": "Atualizando…" + }, + "update.page.complete": { + "message": "Atualização concluída" + }, + "update.page.failed": { + "message": "Falha na atualização" + }, + "window.title.settings": { + "message": "Configurações" + }, + "window.title.signIn": { + "message": "Login" + }, + "window.title.sessionExpiration": { + "message": "Sessão expirando" + }, + "window.title.updating": { + "message": "Atualizando" + }, + "window.title.welcome": { + "message": "Bem-vindo ao NetBird" + }, + "window.title.error": { + "message": "Erro" + }, + "welcome.title": { + "message": "Procure o NetBird na sua bandeja" + }, + "welcome.description": { + "message": "O NetBird fica na sua bandeja. Clique no ícone para conectar, alternar perfis ou abrir as configurações." + }, + "welcome.continue": { + "message": "Continuar" + }, + "welcome.back": { + "message": "Voltar" + }, + "welcome.management.title": { + "message": "Configurar o NetBird" + }, + "welcome.management.description": { + "message": "Clique em Continuar para começar ou escolha Auto-hospedado se você tiver o seu próprio servidor NetBird." + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud" + }, + "welcome.management.cloud.description": { + "message": "Use o nosso serviço hospedado. Sem configuração necessária." + }, + "welcome.management.selfHosted.title": { + "message": "Auto-hospedado" + }, + "welcome.management.selfHosted.description": { + "message": "Conecte ao seu próprio servidor de gerenciamento." + }, + "welcome.management.urlLabel": { + "message": "URL do servidor de gerenciamento" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "Insira uma URL válida, ex.: https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "Não foi possível acessar este servidor. Verifique a URL ou a sua rede e continue se tiver certeza de que está correta." + }, + "welcome.management.checking": { + "message": "Verificando…" + }, + "browserLogin.title": { + "message": "Continue no seu navegador para concluir o login" + }, + "browserLogin.notSeeing": { + "message": "Não está vendo a aba do navegador?" + }, + "browserLogin.tryAgain": { + "message": "Tentar novamente" + }, + "browserLogin.openFailedTitle": { + "message": "Falha ao abrir o navegador" + }, + "sessionExpiration.title": { + "message": "A sessão expira em breve" + }, + "sessionExpiration.titleLater": { + "message": "A sua sessão vai expirar" + }, + "sessionExpiration.description": { + "message": "Este dispositivo será desconectado em breve. Renove com um login pelo navegador." + }, + "sessionExpiration.descriptionLater": { + "message": "Um login pelo navegador mantém este dispositivo conectado à sua rede." + }, + "sessionExpiration.stay": { + "message": "Renovar sessão" + }, + "sessionExpiration.authenticate": { + "message": "Autenticar" + }, + "sessionExpiration.logout": { + "message": "Sair" + }, + "sessionExpiration.expired": { + "message": "Sessão expirada" + }, + "sessionExpiration.expiredDescription": { + "message": "Dispositivo desconectado. Autentique com um login pelo navegador para reconectar." + }, + "sessionExpiration.close": { + "message": "Fechar" + }, + "sessionExpiration.extendFailedTitle": { + "message": "Falha ao renovar a sessão" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "Falha ao sair" + }, + "peers.search.placeholder": { + "message": "Buscar por nome ou IP" + }, + "peers.filter.all": { + "message": "Todos" + }, + "peers.filter.online": { + "message": "Online" + }, + "peers.filter.offline": { + "message": "Offline" + }, + "peers.empty.title": { + "message": "Nenhum peer disponível" + }, + "peers.empty.description": { + "message": "Você não tem nenhum peer disponível ou não tem acesso a nenhum deles." + }, + "peers.details.domain": { + "message": "Domínio" + }, + "peers.details.netbirdIp": { + "message": "IP do NetBird" + }, + "peers.details.netbirdIpv6": { + "message": "IPv6 do NetBird" + }, + "peers.details.publicKey": { + "message": "Chave pública" + }, + "peers.details.connection": { + "message": "Conexão" + }, + "peers.details.latency": { + "message": "Latência" + }, + "peers.details.lastHandshake": { + "message": "Último handshake" + }, + "peers.details.statusSince": { + "message": "Última atualização da conexão" + }, + "peers.details.bytes": { + "message": "Bytes" + }, + "peers.details.bytesSent": { + "message": "Enviados" + }, + "peers.details.bytesReceived": { + "message": "Recebidos" + }, + "peers.details.localIce": { + "message": "ICE local" + }, + "peers.details.remoteIce": { + "message": "ICE remoto" + }, + "peers.details.never": { + "message": "Nunca" + }, + "peers.details.justNow": { + "message": "Agora mesmo" + }, + "peers.details.refresh": { + "message": "Atualizar" + }, + "peers.status.connected": { + "message": "Conectado" + }, + "peers.status.connecting": { + "message": "Conectando" + }, + "peers.status.disconnected": { + "message": "Desconectado" + }, + "peers.details.relayAddress": { + "message": "Relay" + }, + "peers.details.networks": { + "message": "Recursos" + }, + "peers.details.relayed": { + "message": "Via relay" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "Rosenpass ativado" + }, + "networks.search.placeholder": { + "message": "Buscar por rede ou domínio" + }, + "networks.filter.all": { + "message": "Todos" + }, + "networks.filter.active": { + "message": "Ativos" + }, + "networks.filter.overlapping": { + "message": "Sobrepostos" + }, + "networks.empty.title": { + "message": "Nenhum recurso disponível" + }, + "networks.empty.description": { + "message": "Você não tem nenhum recurso de rede disponível ou não tem acesso a nenhum deles." + }, + "networks.selected": { + "message": "Selecionado" + }, + "networks.unselected": { + "message": "Não selecionado" + }, + "networks.ips.heading": { + "message": "IPs resolvidos" + }, + "networks.bulk.selectionCount": { + "message": "{selected} de {total} ativos" + }, + "networks.bulk.enableAll": { + "message": "Ativar todos" + }, + "networks.bulk.disableAll": { + "message": "Desativar todos" + }, + "exitNodes.search.placeholder": { + "message": "Buscar nós de saída" + }, + "exitNodes.none": { + "message": "Nenhum" + }, + "exitNodes.empty.title": { + "message": "Nenhum nó de saída disponível" + }, + "exitNodes.empty.description": { + "message": "Nenhum nó de saída foi compartilhado com este peer." + }, + "exitNodes.card.title": { + "message": "Nó de saída" + }, + "exitNodes.card.statusActive": { + "message": "Ativo" + }, + "exitNodes.card.statusInactive": { + "message": "Inativo" + }, + "exitNodes.dropdown.noneTitle": { + "message": "Nenhum" + }, + "exitNodes.dropdown.noneDescription": { + "message": "Conexão direta sem um nó de saída" + }, + "quickActions.connect": { + "message": "Conectar" + }, + "quickActions.disconnect": { + "message": "Desconectar" + }, + "daemon.unavailable.title": { + "message": "O serviço do NetBird não está em execução" + }, + "daemon.unavailable.description": { + "message": "O aplicativo reconectará automaticamente assim que o serviço estiver em execução." + }, + "daemon.unavailable.docsLink": { + "message": "Documentação" + }, + "daemon.outdated.title": { + "message": "O serviço NetBird está desatualizado" + }, + "daemon.outdated.description": { + "message": "Atualize o serviço NetBird para usar este aplicativo." + }, + "error.jwt_clock_skew": { + "message": "Falha no login: o relógio deste dispositivo está fora de sincronia com o servidor. Sincronize o relógio do sistema e tente novamente." + }, + "error.jwt_expired": { + "message": "O seu token de login expirou. Faça login novamente." + }, + "error.jwt_signature_invalid": { + "message": "Falha no login: a assinatura do token é inválida. Entre em contato com o seu administrador." + }, + "error.session_expired": { + "message": "A sua sessão expirou. Faça login novamente." + }, + "error.invalid_setup_key": { + "message": "A chave de configuração está ausente ou é inválida." + }, + "error.permission_denied": { + "message": "O login foi rejeitado pelo servidor." + }, + "error.daemon_unreachable": { + "message": "O daemon do NetBird não está respondendo. Verifique se o serviço está em execução." + }, + "error.unknown": { + "message": "A operação falhou." + } +} diff --git a/client/ui/i18n/locales/ru/common.json b/client/ui/i18n/locales/ru/common.json new file mode 100644 index 000000000..b9dcc0af7 --- /dev/null +++ b/client/ui/i18n/locales/ru/common.json @@ -0,0 +1,1331 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "Отключено" + }, + "tray.status.daemonUnavailable": { + "message": "Не запущено" + }, + "tray.status.error": { + "message": "Ошибка" + }, + "tray.status.connected": { + "message": "Подключено" + }, + "tray.status.connecting": { + "message": "Подключение" + }, + "tray.status.needsLogin": { + "message": "Требуется вход" + }, + "tray.status.loginFailed": { + "message": "Ошибка входа" + }, + "tray.status.sessionExpired": { + "message": "Сеанс истёк" + }, + "tray.session.expiresIn": { + "message": "Сеанс истекает через {remaining}" + }, + "tray.session.unit.lessThanMinute": { + "message": "менее чем минуту" + }, + "tray.session.unit.minute": { + "message": "1 минуту" + }, + "tray.session.unit.minutes": { + "message": "{count} минут" + }, + "tray.session.unit.hour": { + "message": "1 час" + }, + "tray.session.unit.hours": { + "message": "{count} часов" + }, + "tray.session.unit.day": { + "message": "1 день" + }, + "tray.session.unit.days": { + "message": "{count} дней" + }, + "tray.menu.open": { + "message": "Открыть NetBird" + }, + "tray.menu.connect": { + "message": "Подключиться" + }, + "tray.menu.disconnect": { + "message": "Отключиться" + }, + "tray.menu.exitNode": { + "message": "Выходной узел" + }, + "tray.menu.networks": { + "message": "Ресурсы" + }, + "tray.menu.profiles": { + "message": "Профили" + }, + "tray.menu.manageProfiles": { + "message": "Управление профилями" + }, + "tray.menu.settings": { + "message": "Настройки…" + }, + "tray.menu.debugBundle": { + "message": "Создать отладочный пакет" + }, + "tray.menu.about": { + "message": "Помощь и поддержка" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "Документация" + }, + "tray.menu.troubleshoot": { + "message": "Диагностика" + }, + "tray.menu.downloadLatest": { + "message": "Загрузить последнюю версию" + }, + "tray.menu.installVersion": { + "message": "Установить версию {version}" + }, + "tray.menu.guiVersion": { + "message": "GUI: {version}" + }, + "tray.menu.daemonVersion": { + "message": "Демон: {version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "Выйти из NetBird" + }, + "notify.daemonOutdated.title": { + "message": "Служба NetBird устарела" + }, + "notify.daemonOutdated.body": { + "message": "Обновите службу NetBird, чтобы использовать это приложение." + }, + "notify.update.title": { + "message": "Доступно обновление NetBird" + }, + "notify.update.body": { + "message": "Доступна версия NetBird {version}." + }, + "notify.update.enforcedSuffix": { + "message": " Ваш администратор требует установить это обновление." + }, + "notify.error.title": { + "message": "Ошибка" + }, + "notify.error.connect": { + "message": "Не удалось подключиться" + }, + "notify.error.disconnect": { + "message": "Не удалось отключиться" + }, + "notify.error.switchProfile": { + "message": "Не удалось переключиться на {profile}" + }, + "notify.error.exitNode": { + "message": "Не удалось обновить выходной узел {name}" + }, + "notify.sessionExpired.title": { + "message": "Сеанс NetBird истёк" + }, + "notify.sessionExpired.body": { + "message": "Сеанс NetBird истёк. Пожалуйста, войдите снова." + }, + "notify.sessionWarning.title": { + "message": "Сеанс скоро истечёт" + }, + "notify.sessionWarning.body": { + "message": "Сеанс NetBird истекает через {remaining}. Нажмите «Продлить сейчас», чтобы продлить сеанс." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "Сеанс NetBird скоро истечёт. Нажмите «Продлить сейчас», чтобы продлить сеанс." + }, + "notify.sessionWarning.extend": { + "message": "Продлить сейчас" + }, + "notify.sessionWarning.dismiss": { + "message": "Закрыть" + }, + "notify.sessionWarning.failed": { + "message": "Не удалось продлить сеанс NetBird" + }, + "notify.sessionWarning.successTitle": { + "message": "Сеанс NetBird продлён" + }, + "notify.sessionWarning.successBody": { + "message": "Сеанс обновлён." + }, + "notify.sessionDeadlineRejected.title": { + "message": "Неверный срок действия сеанса" + }, + "notify.sessionDeadlineRejected.body": { + "message": "Сервер передал неверный срок действия сеанса. Пожалуйста, войдите снова." + }, + "notify.mdm.policyApplied.title": { + "message": "Настройки NetBird обновлены" + }, + "notify.mdm.policyApplied.body": { + "message": "Конфигурация NetBird была обновлена в соответствии с вашей ИТ-политикой." + }, + "common.cancel": { + "message": "Отмена" + }, + "common.save": { + "message": "Сохранить" + }, + "common.saveChanges": { + "message": "Сохранить изменения" + }, + "common.saving": { + "message": "Сохранение…" + }, + "common.close": { + "message": "Закрыть" + }, + "common.copy": { + "message": "Копировать" + }, + "common.togglePasswordVisibility": { + "message": "Показать или скрыть пароль" + }, + "common.increase": { + "message": "Увеличить" + }, + "common.decrease": { + "message": "Уменьшить" + }, + "common.delete": { + "message": "Удалить" + }, + "common.create": { + "message": "Создать" + }, + "common.add": { + "message": "Добавить" + }, + "common.remove": { + "message": "Убрать" + }, + "common.refresh": { + "message": "Обновить" + }, + "common.loading": { + "message": "Загрузка…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "Ничего не найдено" + }, + "common.noResults.description": { + "message": "Ничего не найдено. Попробуйте изменить поисковый запрос или фильтры." + }, + "notConnected.title": { + "message": "Отключено" + }, + "notConnected.description": { + "message": "Сначала подключитесь к NetBird, чтобы увидеть подробную информацию о пирах, сетевых ресурсах и выходных узлах." + }, + "connect.status.disconnected": { + "message": "Отключено" + }, + "connect.status.connecting": { + "message": "Подключение…" + }, + "connect.status.connected": { + "message": "Подключено" + }, + "connect.status.disconnecting": { + "message": "Отключение…" + }, + "connect.status.daemonUnavailable": { + "message": "Демон недоступен" + }, + "connect.status.loginRequired": { + "message": "Требуется вход" + }, + "connect.error.loginTitle": { + "message": "Не удалось войти" + }, + "connect.error.connectTitle": { + "message": "Не удалось подключиться" + }, + "connect.error.disconnectTitle": { + "message": "Не удалось отключиться" + }, + "nav.peers.title": { + "message": "Пиры" + }, + "nav.peers.description": { + "message": "{connected} из {total} подключено" + }, + "nav.resources.title": { + "message": "Ресурсы" + }, + "nav.resources.description": { + "message": "{active} из {total} активно" + }, + "nav.exitNode.title": { + "message": "Выходные узлы" + }, + "nav.exitNode.none": { + "message": "Не активен" + }, + "nav.exitNode.using": { + "message": "Через {name}" + }, + "header.openSettings": { + "message": "Открыть настройки" + }, + "header.togglePanel": { + "message": "Показать или скрыть боковую панель" + }, + "profile.selector.loading": { + "message": "Загрузка…" + }, + "profile.selector.noProfile": { + "message": "Нет профиля" + }, + "profile.selector.searchPlaceholder": { + "message": "Поиск профиля по имени…" + }, + "profile.selector.emptyTitle": { + "message": "Профили не найдены" + }, + "profile.selector.emptyDescription": { + "message": "Измените поисковый запрос или создайте новый профиль." + }, + "profile.selector.newProfile": { + "message": "Новый профиль" + }, + "profile.selector.moreOptions": { + "message": "Дополнительные параметры" + }, + "profile.selector.deregister": { + "message": "Отменить регистрацию" + }, + "profile.selector.delete": { + "message": "Удалить" + }, + "profile.selector.switchTo": { + "message": "Переключиться на этот профиль" + }, + "profile.selector.edit": { + "message": "Изменить" + }, + "profile.edit.title": { + "message": "Изменить профиль" + }, + "profile.edit.submit": { + "message": "Сохранить изменения" + }, + "profile.dialog.title": { + "message": "Введите имя профиля" + }, + "profile.dialog.nameLabel": { + "message": "Имя профиля" + }, + "profile.dialog.description": { + "message": "Задайте легко узнаваемое имя для профиля." + }, + "profile.dialog.placeholder": { + "message": "например, работа" + }, + "profile.dialog.submit": { + "message": "Добавить профиль" + }, + "profile.dialog.required": { + "message": "Введите имя профиля, например работа или дом" + }, + "profile.dialog.managementHelp": { + "message": "Используйте NetBird Cloud или собственный сервер." + }, + "profile.dialog.urlUnreachable": { + "message": "Не удалось связаться с сервером. Проверьте URL или всё равно добавьте профиль, если уверены, что он правильный." + }, + "header.menu.settings": { + "message": "Настройки…" + }, + "header.menu.defaultView": { + "message": "Обычный вид" + }, + "header.menu.advancedView": { + "message": "Расширенный вид" + }, + "header.menu.updateAvailable": { + "message": "Доступно обновление" + }, + "header.menu.open": { + "message": "Открыть меню" + }, + "header.profile.switch": { + "message": "Сменить профиль" + }, + "connect.toggle.label": { + "message": "Переключить подключение NetBird" + }, + "connect.localIp.label": { + "message": "Локальные IP-адреса" + }, + "common.search": { + "message": "Поиск" + }, + "common.filter": { + "message": "Фильтр" + }, + "exitNodes.dropdown.trigger": { + "message": "Выбрать выходной узел" + }, + "peers.row.label": { + "message": "Открыть подробности для {name}, {status}" + }, + "peers.dialog.title": { + "message": "Сведения об узле" + }, + "networks.row.toggle": { + "message": "Переключить {name}" + }, + "networks.bulk.label": { + "message": "Переключить все видимые ресурсы" + }, + "settings.nav.label": { + "message": "Разделы настроек" + }, + "profile.switch.title": { + "message": "Переключиться на профиль «{name}»?" + }, + "profile.switch.message": { + "message": "Вы действительно хотите переключить профиль?\nТекущий профиль будет отключён." + }, + "profile.switch.confirm": { + "message": "Подтвердить" + }, + "profile.deregister.title": { + "message": "Отменить регистрацию профиля «{name}»?" + }, + "profile.deregister.message": { + "message": "Вы действительно хотите отменить регистрацию этого профиля?\nДля его использования потребуется войти снова." + }, + "profile.deregister.confirm": { + "message": "Отменить регистрацию" + }, + "profile.delete.title": { + "message": "Удалить профиль «{name}»?" + }, + "profile.delete.message": { + "message": "Вы действительно хотите удалить этот профиль?\nЭто действие нельзя отменить." + }, + "profile.delete.disabledActive": { + "message": "Активные профили нельзя удалить. Переключитесь на другой профиль, прежде чем удалять этот." + }, + "profile.delete.disabledDefault": { + "message": "Профиль по умолчанию нельзя удалить." + }, + "profile.error.switchTitle": { + "message": "Не удалось переключить профиль" + }, + "profile.error.deregisterTitle": { + "message": "Не удалось отменить регистрацию профиля" + }, + "profile.error.deleteTitle": { + "message": "Не удалось удалить профиль" + }, + "profile.error.createTitle": { + "message": "Не удалось создать профиль" + }, + "profile.error.editTitle": { + "message": "Не удалось изменить профиль" + }, + "profile.error.loadTitle": { + "message": "Не удалось загрузить профили" + }, + "profile.dropdown.activeProfile": { + "message": "Активный профиль" + }, + "profile.dropdown.switchProfile": { + "message": "Переключить профиль" + }, + "profile.dropdown.noEmail": { + "message": "Другое" + }, + "profile.dropdown.addProfile": { + "message": "Добавить профиль" + }, + "profile.dropdown.manageProfiles": { + "message": "Управление профилями" + }, + "profile.dropdown.settings": { + "message": "Настройки" + }, + "settings.profiles.section.profiles": { + "message": "Профили" + }, + "settings.profiles.intro": { + "message": "Управляйте несколькими профилями NetBird параллельно — например, рабочими и личными учётными записями или разными серверами управления. Ниже можно добавлять профили, отменять их регистрацию и удалять их." + }, + "settings.profiles.addProfile": { + "message": "Добавить профиль" + }, + "settings.profiles.active": { + "message": "Активен" + }, + "settings.profiles.emptyTitle": { + "message": "Нет профилей" + }, + "settings.profiles.emptyDescription": { + "message": "Создайте профиль, чтобы подключиться к серверу управления NetBird." + }, + "settings.error.loadTitle": { + "message": "Не удалось загрузить настройки" + }, + "settings.error.saveTitle": { + "message": "Не удалось сохранить настройки" + }, + "settings.error.debugBundleTitle": { + "message": "Не удалось создать отладочный пакет" + }, + "settings.tabs.general": { + "message": "Общие" + }, + "settings.tabs.network": { + "message": "Сеть" + }, + "settings.tabs.security": { + "message": "Безопасность" + }, + "settings.tabs.profiles": { + "message": "Профили" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "Дополнительно" + }, + "settings.tabs.troubleshooting": { + "message": "Диагностика" + }, + "settings.tabs.about": { + "message": "О программе" + }, + "settings.tabs.updateAvailable": { + "message": "Доступно обновление" + }, + "settings.general.section.general": { + "message": "Общие" + }, + "settings.general.section.connection": { + "message": "Подключение" + }, + "settings.general.connectOnStartup.label": { + "message": "Подключаться при запуске" + }, + "settings.general.connectOnStartup.help": { + "message": "Автоматически устанавливать подключение при запуске службы." + }, + "settings.general.notifications.label": { + "message": "Уведомления на рабочем столе" + }, + "settings.general.notifications.help": { + "message": "Показывать уведомления о новых обновлениях и событиях подключения." + }, + "settings.general.autostart.label": { + "message": "Запускать интерфейс NetBird при входе" + }, + "settings.general.autostart.help": { + "message": "Автоматически запускать интерфейс NetBird при входе в систему. Это влияет только на графический интерфейс, но не на фоновую службу." + }, + "settings.general.autostart.errorTitle": { + "message": "Не удалось изменить автозапуск" + }, + "settings.general.language.label": { + "message": "Язык интерфейса" + }, + "settings.general.language.help": { + "message": "Выберите язык интерфейса NetBird." + }, + "settings.general.language.search": { + "message": "Поиск языка…" + }, + "settings.general.language.empty": { + "message": "Языки не найдены." + }, + "settings.general.management.label": { + "message": "Сервер управления" + }, + "settings.general.management.help": { + "message": "Подключайтесь к NetBird Cloud или к собственному серверу управления. Изменения вызовут переподключение клиента." + }, + "settings.general.management.cloud": { + "message": "Cloud" + }, + "settings.general.management.selfHosted": { + "message": "Собственный сервер" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "Введите корректный URL, например https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "Не удалось связаться с сервером. Проверьте URL или всё равно сохраните, если уверены, что он правильный." + }, + "settings.general.management.switchCloudTitle": { + "message": "Переключиться на NetBird Cloud?" + }, + "settings.general.management.switchCloudMessage": { + "message": "Это отключит ваш собственный сервер.\nВозможно, потребуется войти снова." + }, + "settings.general.management.switchCloudConfirm": { + "message": "Переключиться на Cloud" + }, + "settings.network.section.connectivity": { + "message": "Подключение" + }, + "settings.network.section.routingDns": { + "message": "Маршрутизация и DNS" + }, + "settings.network.lazy.label": { + "message": "Подключения по требованию" + }, + "settings.network.lazy.help": { + "message": "Вместо постоянно активных подключений NetBird активирует их по требованию — на основе активности или сигналинга." + }, + "settings.network.monitor.label": { + "message": "Переподключаться при смене сети" + }, + "settings.network.monitor.help": { + "message": "Отслеживать сеть и автоматически переподключаться при изменениях, таких как смена Wi-Fi, изменения Ethernet или выход из спящего режима." + }, + "settings.network.dns.label": { + "message": "Включить DNS" + }, + "settings.network.dns.help": { + "message": "Применять управляемые NetBird настройки DNS к системному резолверу." + }, + "settings.network.clientRoutes.label": { + "message": "Включить клиентские маршруты" + }, + "settings.network.clientRoutes.help": { + "message": "Принимать маршруты от других пиров для доступа к их сетям." + }, + "settings.network.serverRoutes.label": { + "message": "Включить серверные маршруты" + }, + "settings.network.serverRoutes.help": { + "message": "Анонсировать локальные маршруты этого хоста другим пирам." + }, + "settings.network.ipv6.label": { + "message": "Включить IPv6" + }, + "settings.network.ipv6.help": { + "message": "Использовать IPv6-адресацию для оверлейной сети NetBird." + }, + "settings.security.section.firewall": { + "message": "Брандмауэр" + }, + "settings.security.section.encryption": { + "message": "Шифрование" + }, + "settings.security.blockInbound.label": { + "message": "Блокировать входящий трафик" + }, + "settings.security.blockInbound.help": { + "message": "Отклонять незапрошенные подключения от пиров к этому устройству и сетям, которые оно маршрутизирует. Исходящий трафик не затрагивается." + }, + "settings.security.blockLan.label": { + "message": "Блокировать доступ к LAN" + }, + "settings.security.blockLan.help": { + "message": "Запрещать пирам доступ к вашей локальной сети и её устройствам, когда это устройство маршрутизирует их трафик." + }, + "settings.security.rosenpass.label": { + "message": "Включить квантовую устойчивость" + }, + "settings.security.rosenpass.help": { + "message": "Добавить постквантовый обмен ключами через Rosenpass поверх WireGuard®." + }, + "settings.security.rosenpassPermissive.label": { + "message": "Включить разрешающий режим" + }, + "settings.security.rosenpassPermissive.help": { + "message": "Разрешать подключения к пирам без поддержки квантовой устойчивости." + }, + "settings.ssh.section.server": { + "message": "Сервер" + }, + "settings.ssh.section.capabilities": { + "message": "Возможности" + }, + "settings.ssh.section.authentication": { + "message": "Аутентификация" + }, + "settings.ssh.server.label": { + "message": "Включить SSH-сервер" + }, + "settings.ssh.server.help": { + "message": "Запускать SSH-сервер NetBird на этом хосте, чтобы другие пиры могли к нему подключаться." + }, + "settings.ssh.root.label": { + "message": "Разрешить вход под root" + }, + "settings.ssh.root.help": { + "message": "Разрешить пирам входить под пользователем root. Отключите, чтобы требовать непривилегированную учётную запись." + }, + "settings.ssh.sftp.label": { + "message": "Разрешить SFTP" + }, + "settings.ssh.sftp.help": { + "message": "Безопасно передавать файлы через стандартные клиенты SFTP или SCP." + }, + "settings.ssh.localForward.label": { + "message": "Локальная переадресация портов" + }, + "settings.ssh.localForward.help": { + "message": "Разрешить подключающимся пирам туннелировать локальные порты к службам, доступным с этого хоста." + }, + "settings.ssh.remoteForward.label": { + "message": "Удалённая переадресация портов" + }, + "settings.ssh.remoteForward.help": { + "message": "Разрешить подключающимся пирам пробрасывать порты этого хоста на свою машину." + }, + "settings.ssh.jwt.label": { + "message": "Включить аутентификацию JWT" + }, + "settings.ssh.jwt.help": { + "message": "Проверять каждую сессию SSH через ваш IdP для идентификации пользователя и аудита. Отключите, чтобы полагаться только на сетевые политики ACL — полезно, когда IdP недоступен." + }, + "settings.ssh.jwtTtl.label": { + "message": "TTL кэша JWT" + }, + "settings.ssh.jwtTtl.help": { + "message": "Как долго этот клиент кэширует JWT, прежде чем снова запрашивать его при исходящих SSH-подключениях. Установите 0, чтобы отключить кэширование и аутентифицироваться при каждом подключении." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "сек." + }, + "settings.advanced.section.interface": { + "message": "Интерфейс" + }, + "settings.advanced.section.security": { + "message": "Безопасность" + }, + "settings.advanced.interfaceName.label": { + "message": "Имя" + }, + "settings.advanced.interfaceName.error": { + "message": "Используйте от 1 до 15 букв, цифр, точек, дефисов или подчёркиваний." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "Должно начинаться с «utun», за которым следует число (например, utun100)." + }, + "settings.advanced.port.label": { + "message": "Порт" + }, + "settings.advanced.port.error": { + "message": "Введите порт от {min} до {max}." + }, + "settings.advanced.port.help": { + "message": "Если задано 0, будет использован случайный свободный порт." + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "Введите значение MTU от {min} до {max}." + }, + "settings.advanced.psk.label": { + "message": "Общий ключ" + }, + "settings.advanced.psk.help": { + "message": "Необязательный PSK WireGuard для дополнительного симметричного шифрования. Это не то же самое, что ключ установки NetBird. Вы будете обмениваться данными только с пирами, использующими тот же общий ключ." + }, + "settings.troubleshooting.section.title": { + "message": "Отладочный пакет" + }, + "settings.troubleshooting.anonymize.label": { + "message": "Анонимизировать конфиденциальную информацию" + }, + "settings.troubleshooting.anonymize.help": { + "message": "Скрывает публичные IP-адреса и сторонние (не относящиеся к NetBird) домены в журналах." + }, + "settings.troubleshooting.systemInfo.label": { + "message": "Включить сведения о системе" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "Включить ОС, ядро, сетевые интерфейсы и таблицы маршрутизации." + }, + "settings.troubleshooting.upload.label": { + "message": "Загрузить пакет на серверы NetBird" + }, + "settings.troubleshooting.upload.help": { + "message": "Возвращает ключ загрузки, который можно передать поддержке NetBird." + }, + "settings.troubleshooting.trace.label": { + "message": "Включить журналы TRACE" + }, + "settings.troubleshooting.trace.help": { + "message": "Повышает уровень журналирования до TRACE и затем восстанавливает прежний." + }, + "settings.troubleshooting.capture.label": { + "message": "Сеанс записи" + }, + "settings.troubleshooting.capture.help": { + "message": "Переподключается и ожидает, чтобы вы могли воспроизвести проблему." + }, + "settings.troubleshooting.packets.label": { + "message": "Записывать сетевые пакеты" + }, + "settings.troubleshooting.packets.help": { + "message": "Сохраняет .pcap сетевого трафика во время сеанса записи." + }, + "settings.troubleshooting.duration.label": { + "message": "Длительность записи" + }, + "settings.troubleshooting.duration.help": { + "message": "Как долго длится сеанс записи." + }, + "settings.troubleshooting.duration.suffix": { + "message": "мин." + }, + "settings.troubleshooting.create": { + "message": "Создать отладочный пакет" + }, + "settings.troubleshooting.progress.description": { + "message": "Сбор журналов, сведений о системе и состояния подключения. Обычно это занимает несколько секунд — не закрывайте это окно до завершения." + }, + "settings.troubleshooting.cancelling": { + "message": "Отмена…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "Отладочный пакет успешно загружен!" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "Пакет сохранён" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "Поделитесь ключом загрузки ниже с поддержкой NetBird. Локальная копия также сохранена на вашем устройстве." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "Отладочный пакет сохранён локально." + }, + "settings.troubleshooting.done.copyKey": { + "message": "Копировать ключ" + }, + "settings.troubleshooting.done.openFolder": { + "message": "Открыть папку" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "Открыть расположение файла" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "Не удалось загрузить: {reason} Пакет всё равно сохранён локально." + }, + "settings.troubleshooting.uploadFailed": { + "message": "Не удалось загрузить. Пакет всё равно сохранён локально." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "Переподключение NetBird…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "Сбор отладочных журналов" + }, + "settings.troubleshooting.stage.bundling": { + "message": "Создание отладочного пакета…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "Загрузка в NetBird…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "Отмена…" + }, + "settings.about.client": { + "message": "Клиент NetBird v{version}" + }, + "settings.about.clientName": { + "message": "Клиент NetBird" + }, + "settings.about.development": { + "message": "[Разработка]" + }, + "settings.about.gui": { + "message": "GUI v{version}" + }, + "settings.about.guiName": { + "message": "GUI" + }, + "settings.about.copyright": { + "message": "© {year} NetBird. Все права защищены." + }, + "settings.about.links.imprint": { + "message": "Правовая информация" + }, + "settings.about.links.privacy": { + "message": "Конфиденциальность" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "Условия использования" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "Форум" + }, + "settings.about.community.documentation": { + "message": "Документация" + }, + "settings.about.community.feedback": { + "message": "Обратная связь" + }, + "update.banner.message": { + "message": "NetBird {version} готов к установке." + }, + "update.banner.later": { + "message": "Позже" + }, + "update.banner.installNow": { + "message": "Установить сейчас" + }, + "update.card.versionAvailableDownload": { + "message": "Версия {version} доступна для загрузки." + }, + "update.card.versionAvailableInstall": { + "message": "Версия {version} доступна для установки." + }, + "update.card.whatsNew": { + "message": "Что нового?" + }, + "update.card.installNow": { + "message": "Установить сейчас" + }, + "update.card.getInstaller": { + "message": "Загрузить" + }, + "update.card.autoCheckInterval": { + "message": "NetBird проверяет обновления в фоновом режиме." + }, + "update.card.changelog": { + "message": "Список изменений" + }, + "update.card.onLatestVersion": { + "message": "У вас установлена последняя версия" + }, + "update.header.tooltip": { + "message": "Доступно обновление" + }, + "update.overlay.updatingVersion": { + "message": "Обновление NetBird до v{version}" + }, + "update.overlay.updating": { + "message": "Обновление NetBird" + }, + "update.overlay.description": { + "message": "Доступна более новая версия, идёт её установка. NetBird автоматически перезапустится после завершения обновления." + }, + "update.overlay.error.timeoutTitle": { + "message": "Обновление занимает слишком много времени" + }, + "update.overlay.error.timeoutDescription": { + "message": "Установка {target} заняла слишком много времени и не завершилась." + }, + "update.overlay.error.canceledTitle": { + "message": "Обновление остановлено" + }, + "update.overlay.error.canceledDescription": { + "message": "Обновление до {target} было отменено до завершения." + }, + "update.overlay.error.failTitle": { + "message": "Не удалось установить обновление" + }, + "update.overlay.error.failDescription": { + "message": "Не удалось установить обновление до {target}." + }, + "update.overlay.error.unknownMessage": { + "message": "неизвестная ошибка" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "новой версии" + }, + "update.error.loadStateTitle": { + "message": "Не удалось загрузить состояние обновления" + }, + "update.error.triggerTitle": { + "message": "Не удалось запустить обновление" + }, + "update.page.versionLine": { + "message": "Обновление клиента до версии {version}." + }, + "update.page.versionLineGeneric": { + "message": "Обновление клиента." + }, + "update.page.outdated": { + "message": "Версия вашего клиента старше версии автообновления, заданной на сервере управления." + }, + "update.page.status.running": { + "message": "Обновление" + }, + "update.page.status.timeout": { + "message": "Время ожидания обновления истекло. Повторите попытку." + }, + "update.page.status.canceled": { + "message": "Обновление отменено." + }, + "update.page.status.failed": { + "message": "Не удалось обновить: {message}" + }, + "update.page.status.unknownError": { + "message": "неизвестная ошибка обновления" + }, + "update.page.failedTitle": { + "message": "Не удалось обновить" + }, + "update.page.timeoutMessage": { + "message": "Время ожидания обновления истекло." + }, + "update.page.dontClose": { + "message": "Пожалуйста, не закрывайте это окно." + }, + "update.page.updating": { + "message": "Обновление…" + }, + "update.page.complete": { + "message": "Обновление завершено" + }, + "update.page.failed": { + "message": "Обновление не удалось" + }, + "window.title.settings": { + "message": "Настройки" + }, + "window.title.signIn": { + "message": "Вход" + }, + "window.title.sessionExpiration": { + "message": "Истечение сеанса" + }, + "window.title.updating": { + "message": "Обновление" + }, + "window.title.welcome": { + "message": "Добро пожаловать в NetBird" + }, + "window.title.error": { + "message": "Ошибка" + }, + "welcome.title": { + "message": "Найдите NetBird в системном трее" + }, + "welcome.description": { + "message": "NetBird находится в системном трее. Нажмите на значок, чтобы подключиться, переключить профиль или открыть настройки." + }, + "welcome.continue": { + "message": "Продолжить" + }, + "welcome.back": { + "message": "Назад" + }, + "welcome.management.title": { + "message": "Настройка NetBird" + }, + "welcome.management.description": { + "message": "Нажмите «Продолжить», чтобы начать, или выберите «Собственный сервер», если у вас есть свой сервер NetBird." + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud" + }, + "welcome.management.cloud.description": { + "message": "Используйте наш облачный сервис. Настройка не требуется." + }, + "welcome.management.selfHosted.title": { + "message": "Собственный сервер" + }, + "welcome.management.selfHosted.description": { + "message": "Подключитесь к собственному серверу управления." + }, + "welcome.management.urlLabel": { + "message": "URL сервера управления" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "Введите корректный URL, например https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "Не удалось связаться с сервером. Проверьте URL или сеть, затем продолжите, если уверены, что он правильный." + }, + "welcome.management.checking": { + "message": "Проверка…" + }, + "browserLogin.title": { + "message": "Завершите вход в браузере" + }, + "browserLogin.notSeeing": { + "message": "Мы открыли вкладку браузера, чтобы вы могли войти в аккаунт. Не видите вкладку?" + }, + "browserLogin.tryAgain": { + "message": "Повторить" + }, + "browserLogin.openFailedTitle": { + "message": "Не удалось открыть браузер" + }, + "sessionExpiration.title": { + "message": "Сеанс скоро истечёт" + }, + "sessionExpiration.titleLater": { + "message": "Ваш сеанс истечёт" + }, + "sessionExpiration.description": { + "message": "Это устройство скоро будет отключено. Продлите сеанс через вход в браузере." + }, + "sessionExpiration.descriptionLater": { + "message": "Вход в браузере сохранит подключение этого устройства к вашей сети." + }, + "sessionExpiration.stay": { + "message": "Продлить сеанс" + }, + "sessionExpiration.authenticate": { + "message": "Войти" + }, + "sessionExpiration.logout": { + "message": "Выйти" + }, + "sessionExpiration.expired": { + "message": "Сеанс истёк" + }, + "sessionExpiration.expiredDescription": { + "message": "Устройство отключено. Войдите через браузер, чтобы переподключиться." + }, + "sessionExpiration.close": { + "message": "Закрыть" + }, + "sessionExpiration.extendFailedTitle": { + "message": "Не удалось продлить сеанс" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "Не удалось выйти" + }, + "peers.search.placeholder": { + "message": "Поиск по имени или IP" + }, + "peers.filter.all": { + "message": "Все" + }, + "peers.filter.online": { + "message": "В сети" + }, + "peers.filter.offline": { + "message": "Не в сети" + }, + "peers.empty.title": { + "message": "Нет доступных пиров" + }, + "peers.empty.description": { + "message": "У вас нет доступных пиров или нет доступа ни к одному из них." + }, + "peers.details.domain": { + "message": "Домен" + }, + "peers.details.netbirdIp": { + "message": "NetBird IP" + }, + "peers.details.netbirdIpv6": { + "message": "NetBird IPv6" + }, + "peers.details.publicKey": { + "message": "Открытый ключ" + }, + "peers.details.connection": { + "message": "Подключение" + }, + "peers.details.latency": { + "message": "Задержка" + }, + "peers.details.lastHandshake": { + "message": "Последнее рукопожатие" + }, + "peers.details.statusSince": { + "message": "Последнее обновление подключения" + }, + "peers.details.bytes": { + "message": "Байты" + }, + "peers.details.bytesSent": { + "message": "Отправлено" + }, + "peers.details.bytesReceived": { + "message": "Получено" + }, + "peers.details.localIce": { + "message": "Локальный ICE" + }, + "peers.details.remoteIce": { + "message": "Удалённый ICE" + }, + "peers.details.never": { + "message": "Никогда" + }, + "peers.details.justNow": { + "message": "Только что" + }, + "peers.details.refresh": { + "message": "Обновить" + }, + "peers.status.connected": { + "message": "Подключено" + }, + "peers.status.connecting": { + "message": "Подключение" + }, + "peers.status.disconnected": { + "message": "Отключено" + }, + "peers.details.relayAddress": { + "message": "Ретранслятор" + }, + "peers.details.networks": { + "message": "Ресурсы" + }, + "peers.details.relayed": { + "message": "Через ретранслятор" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "Rosenpass включён" + }, + "networks.search.placeholder": { + "message": "Поиск по сети или домену" + }, + "networks.filter.all": { + "message": "Все" + }, + "networks.filter.active": { + "message": "Активные" + }, + "networks.filter.overlapping": { + "message": "Пересекающиеся" + }, + "networks.empty.title": { + "message": "Нет доступных ресурсов" + }, + "networks.empty.description": { + "message": "У вас нет доступных сетевых ресурсов или нет доступа ни к одному из них." + }, + "networks.selected": { + "message": "Выбрано" + }, + "networks.unselected": { + "message": "Не выбрано" + }, + "networks.ips.heading": { + "message": "Распознанные IP-адреса" + }, + "networks.bulk.selectionCount": { + "message": "{selected} из {total} активно" + }, + "networks.bulk.enableAll": { + "message": "Включить все" + }, + "networks.bulk.disableAll": { + "message": "Отключить все" + }, + "exitNodes.search.placeholder": { + "message": "Поиск выходных узлов" + }, + "exitNodes.none": { + "message": "Нет" + }, + "exitNodes.empty.title": { + "message": "Нет доступных выходных узлов" + }, + "exitNodes.empty.description": { + "message": "Этому пиру не предоставлены выходные узлы." + }, + "exitNodes.card.title": { + "message": "Выходной узел" + }, + "exitNodes.card.statusActive": { + "message": "Активен" + }, + "exitNodes.card.statusInactive": { + "message": "Неактивен" + }, + "exitNodes.dropdown.noneTitle": { + "message": "Нет" + }, + "exitNodes.dropdown.noneDescription": { + "message": "Прямое подключение без выходного узла" + }, + "quickActions.connect": { + "message": "Подключиться" + }, + "quickActions.disconnect": { + "message": "Отключиться" + }, + "daemon.unavailable.title": { + "message": "Служба NetBird не запущена" + }, + "daemon.unavailable.description": { + "message": "Приложение автоматически переподключится, как только служба будет запущена." + }, + "daemon.unavailable.docsLink": { + "message": "Документация" + }, + "daemon.outdated.title": { + "message": "Служба NetBird устарела" + }, + "daemon.outdated.description": { + "message": "Обновите службу NetBird, чтобы использовать это приложение." + }, + "error.jwt_clock_skew": { + "message": "Не удалось войти: часы этого устройства рассинхронизированы с сервером. Синхронизируйте системные часы и повторите попытку." + }, + "error.jwt_expired": { + "message": "Срок действия токена входа истёк. Войдите снова." + }, + "error.jwt_signature_invalid": { + "message": "Не удалось войти: недействительная подпись токена. Обратитесь к администратору." + }, + "error.session_expired": { + "message": "Ваш сеанс истёк. Войдите снова." + }, + "error.invalid_setup_key": { + "message": "Ключ установки отсутствует или недействителен." + }, + "error.permission_denied": { + "message": "Сервер отклонил вход." + }, + "error.daemon_unreachable": { + "message": "Демон NetBird не отвечает. Проверьте, запущена ли служба." + }, + "error.unknown": { + "message": "Не удалось выполнить операцию." + } +} diff --git a/client/ui/i18n/locales/zh-CN/common.json b/client/ui/i18n/locales/zh-CN/common.json new file mode 100644 index 000000000..6c3f8fcda --- /dev/null +++ b/client/ui/i18n/locales/zh-CN/common.json @@ -0,0 +1,1331 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "已断开连接" + }, + "tray.status.daemonUnavailable": { + "message": "未运行" + }, + "tray.status.error": { + "message": "错误" + }, + "tray.status.connected": { + "message": "已连接" + }, + "tray.status.connecting": { + "message": "正在连接" + }, + "tray.status.needsLogin": { + "message": "需要登录" + }, + "tray.status.loginFailed": { + "message": "登录失败" + }, + "tray.status.sessionExpired": { + "message": "会话已过期" + }, + "tray.session.expiresIn": { + "message": "会话将在 {remaining} 后过期" + }, + "tray.session.unit.lessThanMinute": { + "message": "不到一分钟" + }, + "tray.session.unit.minute": { + "message": "1 分钟" + }, + "tray.session.unit.minutes": { + "message": "{count} 分钟" + }, + "tray.session.unit.hour": { + "message": "1 小时" + }, + "tray.session.unit.hours": { + "message": "{count} 小时" + }, + "tray.session.unit.day": { + "message": "1 天" + }, + "tray.session.unit.days": { + "message": "{count} 天" + }, + "tray.menu.open": { + "message": "打开 NetBird" + }, + "tray.menu.connect": { + "message": "连接" + }, + "tray.menu.disconnect": { + "message": "断开连接" + }, + "tray.menu.exitNode": { + "message": "出口节点" + }, + "tray.menu.networks": { + "message": "资源" + }, + "tray.menu.profiles": { + "message": "配置文件" + }, + "tray.menu.manageProfiles": { + "message": "管理配置文件" + }, + "tray.menu.settings": { + "message": "设置…" + }, + "tray.menu.debugBundle": { + "message": "创建调试包" + }, + "tray.menu.about": { + "message": "帮助与支持" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "文档" + }, + "tray.menu.troubleshoot": { + "message": "故障排除" + }, + "tray.menu.downloadLatest": { + "message": "下载最新版本" + }, + "tray.menu.installVersion": { + "message": "安装 {version} 版本" + }, + "tray.menu.guiVersion": { + "message": "GUI:{version}" + }, + "tray.menu.daemonVersion": { + "message": "守护进程:{version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "退出 NetBird" + }, + "notify.daemonOutdated.title": { + "message": "NetBird 服务版本过旧" + }, + "notify.daemonOutdated.body": { + "message": "请更新 NetBird 服务以使用此应用。" + }, + "notify.update.title": { + "message": "NetBird 有可用更新" + }, + "notify.update.body": { + "message": "NetBird {version} 已可用。" + }, + "notify.update.enforcedSuffix": { + "message": " 您的管理员要求进行此次更新。" + }, + "notify.error.title": { + "message": "错误" + }, + "notify.error.connect": { + "message": "连接失败" + }, + "notify.error.disconnect": { + "message": "断开连接失败" + }, + "notify.error.switchProfile": { + "message": "切换到 {profile} 失败" + }, + "notify.error.exitNode": { + "message": "更新出口节点 {name} 失败" + }, + "notify.sessionExpired.title": { + "message": "NetBird 会话已过期" + }, + "notify.sessionExpired.body": { + "message": "您的 NetBird 会话已过期。请重新登录。" + }, + "notify.sessionWarning.title": { + "message": "会话即将过期" + }, + "notify.sessionWarning.body": { + "message": "您的 NetBird 会话将在 {remaining} 后过期。点击“立即延长”以续期。" + }, + "notify.sessionWarning.bodyGeneric": { + "message": "您的 NetBird 会话即将过期。点击“立即延长”以续期。" + }, + "notify.sessionWarning.extend": { + "message": "立即延长" + }, + "notify.sessionWarning.dismiss": { + "message": "忽略" + }, + "notify.sessionWarning.failed": { + "message": "延长 NetBird 会话失败" + }, + "notify.sessionWarning.successTitle": { + "message": "NetBird 会话已延长" + }, + "notify.sessionWarning.successBody": { + "message": "您的会话已刷新。" + }, + "notify.sessionDeadlineRejected.title": { + "message": "会话截止时间被拒绝" + }, + "notify.sessionDeadlineRejected.body": { + "message": "服务器发送了无效的会话截止时间。请重新登录。" + }, + "notify.mdm.policyApplied.title": { + "message": "NetBird 设置已更新" + }, + "notify.mdm.policyApplied.body": { + "message": "您的 NetBird 配置已根据 IT 策略更新。" + }, + "common.cancel": { + "message": "取消" + }, + "common.save": { + "message": "保存" + }, + "common.saveChanges": { + "message": "保存更改" + }, + "common.saving": { + "message": "正在保存…" + }, + "common.close": { + "message": "关闭" + }, + "common.copy": { + "message": "复制" + }, + "common.togglePasswordVisibility": { + "message": "切换密码可见性" + }, + "common.increase": { + "message": "增加" + }, + "common.decrease": { + "message": "减少" + }, + "common.delete": { + "message": "删除" + }, + "common.create": { + "message": "创建" + }, + "common.add": { + "message": "添加" + }, + "common.remove": { + "message": "移除" + }, + "common.refresh": { + "message": "刷新" + }, + "common.loading": { + "message": "正在加载…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "未找到任何结果" + }, + "common.noResults.description": { + "message": "我们未能找到任何结果。请尝试其他搜索词或更改筛选条件。" + }, + "notConnected.title": { + "message": "已断开连接" + }, + "notConnected.description": { + "message": "请先连接到 NetBird,以查看有关对等节点、网络资源和出口节点的详细信息。" + }, + "connect.status.disconnected": { + "message": "已断开连接" + }, + "connect.status.connecting": { + "message": "正在连接…" + }, + "connect.status.connected": { + "message": "已连接" + }, + "connect.status.disconnecting": { + "message": "正在断开连接…" + }, + "connect.status.daemonUnavailable": { + "message": "守护进程不可用" + }, + "connect.status.loginRequired": { + "message": "需要登录" + }, + "connect.error.loginTitle": { + "message": "登录失败" + }, + "connect.error.connectTitle": { + "message": "连接失败" + }, + "connect.error.disconnectTitle": { + "message": "断开连接失败" + }, + "nav.peers.title": { + "message": "对等节点" + }, + "nav.peers.description": { + "message": "{total} 个中已连接 {connected} 个" + }, + "nav.resources.title": { + "message": "资源" + }, + "nav.resources.description": { + "message": "{total} 个中已激活 {active} 个" + }, + "nav.exitNode.title": { + "message": "出口节点" + }, + "nav.exitNode.none": { + "message": "未激活" + }, + "nav.exitNode.using": { + "message": "经由 {name}" + }, + "header.openSettings": { + "message": "打开设置" + }, + "header.togglePanel": { + "message": "切换侧边栏" + }, + "profile.selector.loading": { + "message": "正在加载…" + }, + "profile.selector.noProfile": { + "message": "无配置文件" + }, + "profile.selector.searchPlaceholder": { + "message": "按名称搜索配置文件…" + }, + "profile.selector.emptyTitle": { + "message": "未找到配置文件" + }, + "profile.selector.emptyDescription": { + "message": "请尝试其他搜索词或创建新的配置文件。" + }, + "profile.selector.newProfile": { + "message": "新建配置文件" + }, + "profile.selector.moreOptions": { + "message": "更多选项" + }, + "profile.selector.deregister": { + "message": "注销" + }, + "profile.selector.delete": { + "message": "删除" + }, + "profile.selector.switchTo": { + "message": "切换到此配置文件" + }, + "profile.selector.edit": { + "message": "编辑" + }, + "profile.edit.title": { + "message": "编辑配置文件" + }, + "profile.edit.submit": { + "message": "保存更改" + }, + "profile.dialog.title": { + "message": "输入配置文件名称" + }, + "profile.dialog.nameLabel": { + "message": "配置文件名称" + }, + "profile.dialog.description": { + "message": "为您的配置文件设置一个易于识别的名称。" + }, + "profile.dialog.placeholder": { + "message": "例如:工作" + }, + "profile.dialog.submit": { + "message": "添加配置文件" + }, + "profile.dialog.required": { + "message": "请输入配置文件名称,例如:工作、家庭" + }, + "profile.dialog.managementHelp": { + "message": "使用 NetBird Cloud 或您自己的服务器。" + }, + "profile.dialog.urlUnreachable": { + "message": "无法连接到此服务器。请检查 URL,如果确认无误,也可以照常添加该配置文件。" + }, + "header.menu.settings": { + "message": "设置…" + }, + "header.menu.defaultView": { + "message": "默认视图" + }, + "header.menu.advancedView": { + "message": "高级视图" + }, + "header.menu.updateAvailable": { + "message": "有可用更新" + }, + "header.menu.open": { + "message": "打开菜单" + }, + "header.profile.switch": { + "message": "切换配置文件" + }, + "connect.toggle.label": { + "message": "切换 NetBird 连接" + }, + "connect.localIp.label": { + "message": "本地 IP 地址" + }, + "common.search": { + "message": "搜索" + }, + "common.filter": { + "message": "筛选" + }, + "exitNodes.dropdown.trigger": { + "message": "选择出口节点" + }, + "peers.row.label": { + "message": "打开 {name} 的详情,{status}" + }, + "peers.dialog.title": { + "message": "对等节点详情" + }, + "networks.row.toggle": { + "message": "切换 {name}" + }, + "networks.bulk.label": { + "message": "切换所有可见资源" + }, + "settings.nav.label": { + "message": "设置部分" + }, + "profile.switch.title": { + "message": "切换到配置文件“{name}”?" + }, + "profile.switch.message": { + "message": "您确定要切换配置文件吗?\n您当前的配置文件将被断开连接。" + }, + "profile.switch.confirm": { + "message": "确认" + }, + "profile.deregister.title": { + "message": "注销配置文件“{name}”?" + }, + "profile.deregister.message": { + "message": "您确定要注销此配置文件吗?\n您将需要重新登录才能使用它。" + }, + "profile.deregister.confirm": { + "message": "注销" + }, + "profile.delete.title": { + "message": "删除配置文件“{name}”?" + }, + "profile.delete.message": { + "message": "您确定要删除此配置文件吗?\n此操作无法撤销。" + }, + "profile.delete.disabledActive": { + "message": "无法删除处于活动状态的配置文件。请先切换到其他配置文件,再删除此配置文件。" + }, + "profile.delete.disabledDefault": { + "message": "无法删除默认配置文件。" + }, + "profile.error.switchTitle": { + "message": "切换配置文件失败" + }, + "profile.error.deregisterTitle": { + "message": "注销配置文件失败" + }, + "profile.error.deleteTitle": { + "message": "删除配置文件失败" + }, + "profile.error.createTitle": { + "message": "创建配置文件失败" + }, + "profile.error.editTitle": { + "message": "编辑配置文件失败" + }, + "profile.error.loadTitle": { + "message": "加载配置文件失败" + }, + "profile.dropdown.activeProfile": { + "message": "当前配置文件" + }, + "profile.dropdown.switchProfile": { + "message": "切换配置文件" + }, + "profile.dropdown.noEmail": { + "message": "其他" + }, + "profile.dropdown.addProfile": { + "message": "添加配置文件" + }, + "profile.dropdown.manageProfiles": { + "message": "管理配置文件" + }, + "profile.dropdown.settings": { + "message": "设置" + }, + "settings.profiles.section.profiles": { + "message": "配置文件" + }, + "settings.profiles.intro": { + "message": "并行保留多个独立的 NetBird 身份,例如工作和个人账户,或不同的管理服务器。可在下方添加、注销或删除配置文件。" + }, + "settings.profiles.addProfile": { + "message": "添加配置文件" + }, + "settings.profiles.active": { + "message": "活动" + }, + "settings.profiles.emptyTitle": { + "message": "无配置文件" + }, + "settings.profiles.emptyDescription": { + "message": "创建一个配置文件以连接到 NetBird 管理服务器。" + }, + "settings.error.loadTitle": { + "message": "加载设置失败" + }, + "settings.error.saveTitle": { + "message": "保存设置失败" + }, + "settings.error.debugBundleTitle": { + "message": "创建调试包失败" + }, + "settings.tabs.general": { + "message": "常规" + }, + "settings.tabs.network": { + "message": "网络" + }, + "settings.tabs.security": { + "message": "安全" + }, + "settings.tabs.profiles": { + "message": "配置文件" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "高级" + }, + "settings.tabs.troubleshooting": { + "message": "故障排除" + }, + "settings.tabs.about": { + "message": "关于" + }, + "settings.tabs.updateAvailable": { + "message": "有可用更新" + }, + "settings.general.section.general": { + "message": "常规" + }, + "settings.general.section.connection": { + "message": "连接" + }, + "settings.general.connectOnStartup.label": { + "message": "启动时连接" + }, + "settings.general.connectOnStartup.help": { + "message": "在服务启动时自动建立连接。" + }, + "settings.general.notifications.label": { + "message": "桌面通知" + }, + "settings.general.notifications.help": { + "message": "显示有关新更新和连接事件的桌面通知。" + }, + "settings.general.autostart.label": { + "message": "登录时启动 NetBird 界面" + }, + "settings.general.autostart.help": { + "message": "在您登录时自动启动 NetBird 界面。此设置仅影响图形界面,不影响后台服务。" + }, + "settings.general.autostart.errorTitle": { + "message": "更改自启动设置失败" + }, + "settings.general.language.label": { + "message": "显示语言" + }, + "settings.general.language.help": { + "message": "选择 NetBird 界面的语言。" + }, + "settings.general.language.search": { + "message": "搜索语言…" + }, + "settings.general.language.empty": { + "message": "没有匹配的语言。" + }, + "settings.general.management.label": { + "message": "管理服务器" + }, + "settings.general.management.help": { + "message": "连接到 NetBird Cloud 或您自己的自托管管理服务器。更改将使客户端重新连接。" + }, + "settings.general.management.cloud": { + "message": "Cloud" + }, + "settings.general.management.selfHosted": { + "message": "自托管" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "请输入有效的 URL,例如:https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "无法连接到此服务器。请检查 URL,如果确认无误,也可以照常保存。" + }, + "settings.general.management.switchCloudTitle": { + "message": "切换到 NetBird Cloud?" + }, + "settings.general.management.switchCloudMessage": { + "message": "这将断开您的自托管服务器。\n您可能需要重新登录。" + }, + "settings.general.management.switchCloudConfirm": { + "message": "切换到 Cloud" + }, + "settings.network.section.connectivity": { + "message": "连接性" + }, + "settings.network.section.routingDns": { + "message": "路由与 DNS" + }, + "settings.network.lazy.label": { + "message": "懒连接" + }, + "settings.network.lazy.help": { + "message": "NetBird 不会维持始终在线的连接,而是根据活动或信令按需激活连接。" + }, + "settings.network.monitor.label": { + "message": "网络变化时重新连接" + }, + "settings.network.monitor.help": { + "message": "监测网络,并在发生变化时自动重新连接,例如切换 Wi-Fi、以太网变化或从睡眠中恢复。" + }, + "settings.network.dns.label": { + "message": "启用 DNS" + }, + "settings.network.dns.help": { + "message": "将 NetBird 管理的 DNS 设置应用到主机解析器。" + }, + "settings.network.clientRoutes.label": { + "message": "启用客户端路由" + }, + "settings.network.clientRoutes.help": { + "message": "接受来自其他对等节点的路由,以访问它们的网络。" + }, + "settings.network.serverRoutes.label": { + "message": "启用服务器路由" + }, + "settings.network.serverRoutes.help": { + "message": "向其他对等节点通告此主机的本地路由。" + }, + "settings.network.ipv6.label": { + "message": "启用 IPv6" + }, + "settings.network.ipv6.help": { + "message": "为 NetBird 叠加网络使用 IPv6 寻址。" + }, + "settings.security.section.firewall": { + "message": "防火墙" + }, + "settings.security.section.encryption": { + "message": "加密" + }, + "settings.security.blockInbound.label": { + "message": "阻止入站流量" + }, + "settings.security.blockInbound.help": { + "message": "拒绝对等节点向本设备及其路由的任何网络发起的未经请求的连接。出站流量不受影响。" + }, + "settings.security.blockLan.label": { + "message": "阻止 LAN 访问" + }, + "settings.security.blockLan.help": { + "message": "当本设备为对等节点路由流量时,阻止它们访问您的本地网络或其设备。" + }, + "settings.security.rosenpass.label": { + "message": "启用抗量子加密" + }, + "settings.security.rosenpass.help": { + "message": "在 WireGuard® 之上通过 Rosenpass 添加后量子密钥交换。" + }, + "settings.security.rosenpassPermissive.label": { + "message": "启用宽松模式" + }, + "settings.security.rosenpassPermissive.help": { + "message": "允许连接到不支持抗量子加密的对等节点。" + }, + "settings.ssh.section.server": { + "message": "服务器" + }, + "settings.ssh.section.capabilities": { + "message": "功能" + }, + "settings.ssh.section.authentication": { + "message": "身份验证" + }, + "settings.ssh.server.label": { + "message": "启用 SSH 服务器" + }, + "settings.ssh.server.help": { + "message": "在此主机上运行 NetBird SSH 服务器,以便其他对等节点可以连接到它。" + }, + "settings.ssh.root.label": { + "message": "允许 root 登录" + }, + "settings.ssh.root.help": { + "message": "允许对等节点以 root 用户身份登录。禁用后将要求使用非特权账户。" + }, + "settings.ssh.sftp.label": { + "message": "允许 SFTP" + }, + "settings.ssh.sftp.help": { + "message": "使用原生 SFTP 或 SCP 客户端安全地传输文件。" + }, + "settings.ssh.localForward.label": { + "message": "本地端口转发" + }, + "settings.ssh.localForward.help": { + "message": "允许连接的对等节点将本地端口隧道转发到此主机可访问的服务。" + }, + "settings.ssh.remoteForward.label": { + "message": "远程端口转发" + }, + "settings.ssh.remoteForward.help": { + "message": "允许连接的对等节点将此主机上的端口反向暴露到它们自己的机器。" + }, + "settings.ssh.jwt.label": { + "message": "启用 JWT 身份验证" + }, + "settings.ssh.jwt.help": { + "message": "针对您的 IdP 验证每个 SSH 会话,以确认用户身份并进行审计。禁用后将仅依赖网络 ACL 策略,在没有可用 IdP 时很有用。" + }, + "settings.ssh.jwtTtl.label": { + "message": "JWT 缓存 TTL" + }, + "settings.ssh.jwtTtl.help": { + "message": "在出站 SSH 连接再次提示前,此客户端缓存 JWT 的时长。设为 0 可禁用缓存,每次连接都进行身份验证。" + }, + "settings.ssh.jwtTtl.suffix": { + "message": "秒" + }, + "settings.advanced.section.interface": { + "message": "接口" + }, + "settings.advanced.section.security": { + "message": "安全" + }, + "settings.advanced.interfaceName.label": { + "message": "名称" + }, + "settings.advanced.interfaceName.error": { + "message": "请使用 1-15 个字母、数字、点、连字符或下划线。" + }, + "settings.advanced.interfaceName.errorMac": { + "message": "必须以“utun”开头,后跟一个数字(例如 utun100)。" + }, + "settings.advanced.port.label": { + "message": "端口" + }, + "settings.advanced.port.error": { + "message": "请输入介于 {min} 和 {max} 之间的端口。" + }, + "settings.advanced.port.help": { + "message": "如果设为 0,将使用一个随机的空闲端口。" + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "请输入介于 {min} 和 {max} 之间的 MTU 值。" + }, + "settings.advanced.psk.label": { + "message": "预共享密钥" + }, + "settings.advanced.psk.help": { + "message": "可选的 WireGuard PSK,用于额外的对称加密。它与 NetBird 设置密钥不同。您将只能与使用相同预共享密钥的对等节点通信。" + }, + "settings.troubleshooting.section.title": { + "message": "调试包" + }, + "settings.troubleshooting.anonymize.label": { + "message": "匿名化敏感信息" + }, + "settings.troubleshooting.anonymize.help": { + "message": "从日志中隐藏公共 IP 地址和非 NetBird 域名。" + }, + "settings.troubleshooting.systemInfo.label": { + "message": "包含系统信息" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "包含操作系统、内核、网络接口和路由表。" + }, + "settings.troubleshooting.upload.label": { + "message": "将调试包上传到 NetBird 服务器" + }, + "settings.troubleshooting.upload.help": { + "message": "返回一个上传密钥,供您分享给 NetBird 支持团队。" + }, + "settings.troubleshooting.trace.label": { + "message": "启用跟踪日志" + }, + "settings.troubleshooting.trace.help": { + "message": "将日志级别提升到 TRACE,之后再恢复原级别。" + }, + "settings.troubleshooting.capture.label": { + "message": "捕获会话" + }, + "settings.troubleshooting.capture.help": { + "message": "重新连接并等待,以便您复现问题。" + }, + "settings.troubleshooting.packets.label": { + "message": "捕获网络数据包" + }, + "settings.troubleshooting.packets.help": { + "message": "在捕获期间将网络流量保存为 .pcap 文件。" + }, + "settings.troubleshooting.duration.label": { + "message": "捕获时长" + }, + "settings.troubleshooting.duration.help": { + "message": "捕获会话运行的时长。" + }, + "settings.troubleshooting.duration.suffix": { + "message": "分钟" + }, + "settings.troubleshooting.create": { + "message": "创建调试包" + }, + "settings.troubleshooting.progress.description": { + "message": "正在收集日志、系统详情和连接状态。这通常只需片刻——请保持此窗口打开,直到完成。" + }, + "settings.troubleshooting.cancelling": { + "message": "正在取消…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "调试包已成功上传!" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "调试包已保存" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "请将下方的上传密钥分享给 NetBird 支持团队。本地也已保存了一份副本。" + }, + "settings.troubleshooting.done.savedDescription": { + "message": "您的调试包已保存在本地。" + }, + "settings.troubleshooting.done.copyKey": { + "message": "复制密钥" + }, + "settings.troubleshooting.done.openFolder": { + "message": "打开文件夹" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "打开文件位置" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "上传失败:{reason} 调试包仍已保存在本地。" + }, + "settings.troubleshooting.uploadFailed": { + "message": "上传失败。调试包仍已保存在本地。" + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "正在重新连接 NetBird…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "正在捕获调试日志" + }, + "settings.troubleshooting.stage.bundling": { + "message": "正在生成调试包…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "正在上传到 NetBird…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "正在取消…" + }, + "settings.about.client": { + "message": "NetBird 客户端 v{version}" + }, + "settings.about.clientName": { + "message": "NetBird 客户端" + }, + "settings.about.development": { + "message": "[开发版]" + }, + "settings.about.gui": { + "message": "GUI v{version}" + }, + "settings.about.guiName": { + "message": "GUI" + }, + "settings.about.copyright": { + "message": "© {year} NetBird。保留所有权利。" + }, + "settings.about.links.imprint": { + "message": "法律声明" + }, + "settings.about.links.privacy": { + "message": "隐私" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "服务条款" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "论坛" + }, + "settings.about.community.documentation": { + "message": "文档" + }, + "settings.about.community.feedback": { + "message": "反馈" + }, + "update.banner.message": { + "message": "NetBird {version} 已准备好安装。" + }, + "update.banner.later": { + "message": "稍后" + }, + "update.banner.installNow": { + "message": "立即安装" + }, + "update.card.versionAvailableDownload": { + "message": "{version} 版本可供下载。" + }, + "update.card.versionAvailableInstall": { + "message": "{version} 版本可供安装。" + }, + "update.card.whatsNew": { + "message": "更新内容?" + }, + "update.card.installNow": { + "message": "立即安装" + }, + "update.card.getInstaller": { + "message": "下载" + }, + "update.card.autoCheckInterval": { + "message": "NetBird 会在后台检查更新。" + }, + "update.card.changelog": { + "message": "更新日志" + }, + "update.card.onLatestVersion": { + "message": "您已是最新版本" + }, + "update.header.tooltip": { + "message": "有可用更新" + }, + "update.overlay.updatingVersion": { + "message": "正在将 NetBird 更新到 v{version}" + }, + "update.overlay.updating": { + "message": "正在更新 NetBird" + }, + "update.overlay.description": { + "message": "有更新的版本可用,正在安装。更新完成后,NetBird 将自动重启。" + }, + "update.overlay.error.timeoutTitle": { + "message": "更新耗时过长" + }, + "update.overlay.error.timeoutDescription": { + "message": "安装 {target} 耗时过长,未能完成。" + }, + "update.overlay.error.canceledTitle": { + "message": "更新已停止" + }, + "update.overlay.error.canceledDescription": { + "message": "对 {target} 的更新在完成前已被取消。" + }, + "update.overlay.error.failTitle": { + "message": "无法安装更新" + }, + "update.overlay.error.failDescription": { + "message": "无法安装 {target}。" + }, + "update.overlay.error.unknownMessage": { + "message": "未知错误" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "新版本" + }, + "update.error.loadStateTitle": { + "message": "加载更新状态失败" + }, + "update.error.triggerTitle": { + "message": "启动更新失败" + }, + "update.page.versionLine": { + "message": "正在将客户端更新到:{version}。" + }, + "update.page.versionLineGeneric": { + "message": "正在更新客户端。" + }, + "update.page.outdated": { + "message": "您的客户端版本早于管理服务器中设置的自动更新版本。" + }, + "update.page.status.running": { + "message": "正在更新" + }, + "update.page.status.timeout": { + "message": "更新超时。请重试。" + }, + "update.page.status.canceled": { + "message": "更新已取消。" + }, + "update.page.status.failed": { + "message": "更新失败:{message}" + }, + "update.page.status.unknownError": { + "message": "未知的更新错误" + }, + "update.page.failedTitle": { + "message": "更新失败" + }, + "update.page.timeoutMessage": { + "message": "更新超时。" + }, + "update.page.dontClose": { + "message": "请勿关闭此窗口。" + }, + "update.page.updating": { + "message": "正在更新…" + }, + "update.page.complete": { + "message": "更新完成" + }, + "update.page.failed": { + "message": "更新失败" + }, + "window.title.settings": { + "message": "设置" + }, + "window.title.signIn": { + "message": "登录" + }, + "window.title.sessionExpiration": { + "message": "会话即将过期" + }, + "window.title.updating": { + "message": "正在更新" + }, + "window.title.welcome": { + "message": "欢迎使用 NetBird" + }, + "window.title.error": { + "message": "错误" + }, + "welcome.title": { + "message": "在托盘中查找 NetBird" + }, + "welcome.description": { + "message": "NetBird 驻留在您的托盘中。点击图标即可连接、切换配置文件或打开设置。" + }, + "welcome.continue": { + "message": "继续" + }, + "welcome.back": { + "message": "返回" + }, + "welcome.management.title": { + "message": "设置 NetBird" + }, + "welcome.management.description": { + "message": "点击“继续”即可开始;如果您有自己的 NetBird 服务器,请选择“自托管”。" + }, + "welcome.management.cloud.title": { + "message": "NetBird Cloud" + }, + "welcome.management.cloud.description": { + "message": "使用我们的托管服务。无需任何设置。" + }, + "welcome.management.selfHosted.title": { + "message": "自托管" + }, + "welcome.management.selfHosted.description": { + "message": "连接到您自己的管理服务器。" + }, + "welcome.management.urlLabel": { + "message": "管理服务器 URL" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "请输入有效的 URL,例如:https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "无法连接到此服务器。请检查 URL 或您的网络,如果确认无误,再继续。" + }, + "welcome.management.checking": { + "message": "正在检查…" + }, + "browserLogin.title": { + "message": "请在浏览器中继续以完成登录" + }, + "browserLogin.notSeeing": { + "message": "没看到浏览器标签页?" + }, + "browserLogin.tryAgain": { + "message": "重试" + }, + "browserLogin.openFailedTitle": { + "message": "打开浏览器失败" + }, + "sessionExpiration.title": { + "message": "会话即将过期" + }, + "sessionExpiration.titleLater": { + "message": "您的会话即将过期" + }, + "sessionExpiration.description": { + "message": "此设备即将断开连接。通过浏览器登录进行续期。" + }, + "sessionExpiration.descriptionLater": { + "message": "通过浏览器登录可让此设备保持连接到您的网络。" + }, + "sessionExpiration.stay": { + "message": "续期会话" + }, + "sessionExpiration.authenticate": { + "message": "进行身份验证" + }, + "sessionExpiration.logout": { + "message": "退出登录" + }, + "sessionExpiration.expired": { + "message": "会话已过期" + }, + "sessionExpiration.expiredDescription": { + "message": "设备已断开连接。通过浏览器登录进行身份验证以重新连接。" + }, + "sessionExpiration.close": { + "message": "关闭" + }, + "sessionExpiration.extendFailedTitle": { + "message": "延长会话失败" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "退出登录失败" + }, + "peers.search.placeholder": { + "message": "按名称或 IP 搜索" + }, + "peers.filter.all": { + "message": "全部" + }, + "peers.filter.online": { + "message": "在线" + }, + "peers.filter.offline": { + "message": "离线" + }, + "peers.empty.title": { + "message": "无可用的对等节点" + }, + "peers.empty.description": { + "message": "您可能没有任何可用的对等节点,或者无权访问其中任何一个。" + }, + "peers.details.domain": { + "message": "域名" + }, + "peers.details.netbirdIp": { + "message": "NetBird IP" + }, + "peers.details.netbirdIpv6": { + "message": "NetBird IPv6" + }, + "peers.details.publicKey": { + "message": "公钥" + }, + "peers.details.connection": { + "message": "连接" + }, + "peers.details.latency": { + "message": "延迟" + }, + "peers.details.lastHandshake": { + "message": "上次握手" + }, + "peers.details.statusSince": { + "message": "上次连接更新" + }, + "peers.details.bytes": { + "message": "字节" + }, + "peers.details.bytesSent": { + "message": "已发送" + }, + "peers.details.bytesReceived": { + "message": "已接收" + }, + "peers.details.localIce": { + "message": "本地 ICE" + }, + "peers.details.remoteIce": { + "message": "远程 ICE" + }, + "peers.details.never": { + "message": "从不" + }, + "peers.details.justNow": { + "message": "刚刚" + }, + "peers.details.refresh": { + "message": "刷新" + }, + "peers.status.connected": { + "message": "已连接" + }, + "peers.status.connecting": { + "message": "正在连接" + }, + "peers.status.disconnected": { + "message": "已断开连接" + }, + "peers.details.relayAddress": { + "message": "中继" + }, + "peers.details.networks": { + "message": "资源" + }, + "peers.details.relayed": { + "message": "经中继" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "已启用 Rosenpass" + }, + "networks.search.placeholder": { + "message": "按网络或域名搜索" + }, + "networks.filter.all": { + "message": "全部" + }, + "networks.filter.active": { + "message": "活动" + }, + "networks.filter.overlapping": { + "message": "重叠" + }, + "networks.empty.title": { + "message": "无可用资源" + }, + "networks.empty.description": { + "message": "您可能没有任何可用的网络资源,或者无权访问其中任何一个。" + }, + "networks.selected": { + "message": "已选择" + }, + "networks.unselected": { + "message": "未选择" + }, + "networks.ips.heading": { + "message": "已解析的 IP" + }, + "networks.bulk.selectionCount": { + "message": "{total} 个中已激活 {selected} 个" + }, + "networks.bulk.enableAll": { + "message": "全部启用" + }, + "networks.bulk.disableAll": { + "message": "全部禁用" + }, + "exitNodes.search.placeholder": { + "message": "搜索出口节点" + }, + "exitNodes.none": { + "message": "无" + }, + "exitNodes.empty.title": { + "message": "无可用的出口节点" + }, + "exitNodes.empty.description": { + "message": "尚未向此对等节点共享任何出口节点。" + }, + "exitNodes.card.title": { + "message": "出口节点" + }, + "exitNodes.card.statusActive": { + "message": "活动" + }, + "exitNodes.card.statusInactive": { + "message": "非活动" + }, + "exitNodes.dropdown.noneTitle": { + "message": "无" + }, + "exitNodes.dropdown.noneDescription": { + "message": "不使用出口节点的直接连接" + }, + "quickActions.connect": { + "message": "连接" + }, + "quickActions.disconnect": { + "message": "断开连接" + }, + "daemon.unavailable.title": { + "message": "NetBird 服务未运行" + }, + "daemon.unavailable.description": { + "message": "服务运行后,应用将自动重新连接。" + }, + "daemon.unavailable.docsLink": { + "message": "文档" + }, + "daemon.outdated.title": { + "message": "NetBird 服务版本过旧" + }, + "daemon.outdated.description": { + "message": "请更新 NetBird 服务以使用此应用。" + }, + "error.jwt_clock_skew": { + "message": "登录失败:此设备的时钟与服务器不同步。请同步您的系统时钟后重试。" + }, + "error.jwt_expired": { + "message": "您的登录令牌已过期。请重新登录。" + }, + "error.jwt_signature_invalid": { + "message": "登录失败:令牌签名无效。请联系您的管理员。" + }, + "error.session_expired": { + "message": "您的会话已过期。请重新登录。" + }, + "error.invalid_setup_key": { + "message": "设置密钥缺失或无效。" + }, + "error.permission_denied": { + "message": "登录被服务器拒绝。" + }, + "error.daemon_unreachable": { + "message": "NetBird 守护进程无响应。请检查服务是否正在运行。" + }, + "error.unknown": { + "message": "操作失败。" + } +} diff --git a/client/ui/icons.go b/client/ui/icons.go index 874f24fdd..62569fc77 100644 --- a/client/ui/icons.go +++ b/client/ui/icons.go @@ -1,16 +1,10 @@ -//go:build !(linux && 386) && !windows +//go:build !android && !ios && !freebsd && !js package main -import ( - _ "embed" -) +import _ "embed" -//go:embed assets/netbird.png -var iconAbout []byte - -//go:embed assets/netbird-disconnected.png -var iconAboutDisconnected []byte +// Windows reuses these PNGs: multi-frame .ico never redrew under Wails3's NIM_MODIFY, single-frame PNG does. //go:embed assets/netbird-systemtray-connected.png var iconConnected []byte @@ -21,26 +15,90 @@ var iconConnectedDark []byte //go:embed assets/netbird-systemtray-disconnected.png var iconDisconnected []byte -//go:embed assets/netbird-systemtray-update-disconnected.png -var iconUpdateDisconnected []byte - -//go:embed assets/netbird-systemtray-update-disconnected-dark.png -var iconUpdateDisconnectedDark []byte - -//go:embed assets/netbird-systemtray-update-connected.png -var iconUpdateConnected []byte - -//go:embed assets/netbird-systemtray-update-connected-dark.png -var iconUpdateConnectedDark []byte - //go:embed assets/netbird-systemtray-connecting.png var iconConnecting []byte -//go:embed assets/netbird-systemtray-connecting-dark.png -var iconConnectingDark []byte - //go:embed assets/netbird-systemtray-error.png var iconError []byte -//go:embed assets/netbird-systemtray-error-dark.png -var iconErrorDark []byte +//go:embed assets/netbird-systemtray-needs-login.png +var iconNeedsLogin []byte + +//go:embed assets/netbird-systemtray-update-connected.png +var iconUpdateConnected []byte + +//go:embed assets/netbird-systemtray-update-disconnected.png +var iconUpdateDisconnected []byte + +//go:embed assets/netbird-systemtray-connected-macos.png +var iconConnectedMacOS []byte + +//go:embed assets/netbird-systemtray-disconnected-macos.png +var iconDisconnectedMacOS []byte + +//go:embed assets/netbird-systemtray-connecting-macos.png +var iconConnectingMacOS []byte + +//go:embed assets/netbird-systemtray-error-macos.png +var iconErrorMacOS []byte + +//go:embed assets/netbird-systemtray-needs-login-macos.png +var iconNeedsLoginMacOS []byte + +//go:embed assets/netbird-systemtray-update-connected-macos.png +var iconUpdateConnectedMacOS []byte + +//go:embed assets/netbird-systemtray-update-disconnected-macos.png +var iconUpdateDisconnectedMacOS []byte + +// SNI has no template recoloring, so ship an explicit pair: black (*-mono.png) +// for light panels, white (*-mono-dark.png) for dark panels. + +//go:embed assets/netbird-systemtray-connected-mono.png +var iconConnectedMono []byte + +//go:embed assets/netbird-systemtray-connected-mono-dark.png +var iconConnectedMonoDark []byte + +//go:embed assets/netbird-systemtray-connecting-mono.png +var iconConnectingMono []byte + +//go:embed assets/netbird-systemtray-connecting-mono-dark.png +var iconConnectingMonoDark []byte + +//go:embed assets/netbird-systemtray-disconnected-mono.png +var iconDisconnectedMono []byte + +//go:embed assets/netbird-systemtray-disconnected-mono-dark.png +var iconDisconnectedMonoDark []byte + +//go:embed assets/netbird-systemtray-error-mono.png +var iconErrorMono []byte + +//go:embed assets/netbird-systemtray-error-mono-dark.png +var iconErrorMonoDark []byte + +//go:embed assets/netbird-systemtray-needs-login-mono.png +var iconNeedsLoginMono []byte + +//go:embed assets/netbird-systemtray-needs-login-mono-dark.png +var iconNeedsLoginMonoDark []byte + +//go:embed assets/netbird-systemtray-update-connected-mono.png +var iconUpdateConnectedMono []byte + +//go:embed assets/netbird-systemtray-update-connected-mono-dark.png +var iconUpdateConnectedMonoDark []byte + +//go:embed assets/netbird-systemtray-update-disconnected-mono.png +var iconUpdateDisconnectedMono []byte + +//go:embed assets/netbird-systemtray-update-disconnected-mono-dark.png +var iconUpdateDisconnectedMonoDark []byte + +//go:embed assets/netbird.png +var iconWindow []byte + +// Per-platform menu-row icons live in icons_menu_{windows,other}.go. Windows +// uses 16x16: they go into the Win32 check-mark slot (SM_CXMENUCHECK, ~16x16 at +// 100% DPI) which crops anything bigger; macOS/Linux use 24x24. diff --git a/client/ui/icons_menu_darwin.go b/client/ui/icons_menu_darwin.go new file mode 100644 index 000000000..c077a6d2d --- /dev/null +++ b/client/ui/icons_menu_darwin.go @@ -0,0 +1,23 @@ +//go:build darwin + +package main + +import _ "embed" + +// 22px matches the NSMenuItem row text weight (HIG's 18-22 range); +// Windows uses 16px and Linux 24px — see the sibling icons_menu_*.go. + +//go:embed assets/netbird-menu-dot-connected-22.png +var iconMenuDotConnected []byte + +//go:embed assets/netbird-menu-dot-connecting-22.png +var iconMenuDotConnecting []byte + +//go:embed assets/netbird-menu-dot-error-22.png +var iconMenuDotError []byte + +//go:embed assets/netbird-menu-dot-idle-22.png +var iconMenuDotIdle []byte + +//go:embed assets/netbird-menu-dot-offline-22.png +var iconMenuDotOffline []byte diff --git a/client/ui/icons_menu_linux.go b/client/ui/icons_menu_linux.go new file mode 100644 index 000000000..54e0c743a --- /dev/null +++ b/client/ui/icons_menu_linux.go @@ -0,0 +1,22 @@ +//go:build linux + +package main + +import _ "embed" + +// 24x24: GTK4 menu rows render 22–48 px with no downscaling. + +//go:embed assets/netbird-menu-dot-connected.png +var iconMenuDotConnected []byte + +//go:embed assets/netbird-menu-dot-connecting.png +var iconMenuDotConnecting []byte + +//go:embed assets/netbird-menu-dot-error.png +var iconMenuDotError []byte + +//go:embed assets/netbird-menu-dot-idle.png +var iconMenuDotIdle []byte + +//go:embed assets/netbird-menu-dot-offline.png +var iconMenuDotOffline []byte diff --git a/client/ui/icons_menu_windows.go b/client/ui/icons_menu_windows.go new file mode 100644 index 000000000..c08359902 --- /dev/null +++ b/client/ui/icons_menu_windows.go @@ -0,0 +1,24 @@ +//go:build windows + +package main + +import _ "embed" + +// SetMenuItemBitmaps sizes the HBITMAP to SM_CXMENUCHECK/SM_CYMENUCHECK (16x16 +// at 100% DPI); larger bitmaps overflow the row, hence this Windows-only set +// downscaled from the 24x24 originals. + +//go:embed assets/netbird-menu-dot-connected-16.png +var iconMenuDotConnected []byte + +//go:embed assets/netbird-menu-dot-connecting-16.png +var iconMenuDotConnecting []byte + +//go:embed assets/netbird-menu-dot-error-16.png +var iconMenuDotError []byte + +//go:embed assets/netbird-menu-dot-idle-16.png +var iconMenuDotIdle []byte + +//go:embed assets/netbird-menu-dot-offline-16.png +var iconMenuDotOffline []byte diff --git a/client/ui/icons_windows.go b/client/ui/icons_windows.go deleted file mode 100644 index bd57b2690..000000000 --- a/client/ui/icons_windows.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - _ "embed" -) - -//go:embed assets/netbird.ico -var iconAbout []byte - -//go:embed assets/netbird-disconnected.ico -var iconAboutDisconnected []byte - -//go:embed assets/netbird-systemtray-connected.ico -var iconConnected []byte - -//go:embed assets/netbird-systemtray-connected-dark.ico -var iconConnectedDark []byte - -//go:embed assets/netbird-systemtray-disconnected.ico -var iconDisconnected []byte - -//go:embed assets/netbird-systemtray-update-disconnected.ico -var iconUpdateDisconnected []byte - -//go:embed assets/netbird-systemtray-update-disconnected-dark.ico -var iconUpdateDisconnectedDark []byte - -//go:embed assets/netbird-systemtray-update-connected.ico -var iconUpdateConnected []byte - -//go:embed assets/netbird-systemtray-update-connected-dark.ico -var iconUpdateConnectedDark []byte - -//go:embed assets/netbird-systemtray-connecting.ico -var iconConnecting []byte - -//go:embed assets/netbird-systemtray-connecting-dark.ico -var iconConnectingDark []byte - -//go:embed assets/netbird-systemtray-error.ico -var iconError []byte - -//go:embed assets/netbird-systemtray-error-dark.ico -var iconErrorDark []byte diff --git a/client/ui/localizer.go b/client/ui/localizer.go new file mode 100644 index 000000000..33c1cf205 --- /dev/null +++ b/client/ui/localizer.go @@ -0,0 +1,130 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "strings" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/ui/i18n" + "github.com/netbirdio/netbird/client/ui/preferences" + "github.com/netbirdio/netbird/client/ui/services" +) + +// Localizer caches the active language so key lookups skip the preferences store. +// +// Kept in the main package (not i18n/) because StatusLabel maps daemon +// status enum strings to translations; moving it would invert the +// dependency direction. +type Localizer struct { + bundle *i18n.Bundle + store *preferences.Store + + mu sync.RWMutex + lang i18n.LanguageCode + + unsubscribe func() +} + +// NewLocalizer seeds the active language from the on-disk preference. Either +// argument may be nil (tests): T then returns the raw key and Watch is a no-op. +func NewLocalizer(bundle *i18n.Bundle, store *preferences.Store) *Localizer { + l := &Localizer{ + bundle: bundle, + store: store, + lang: i18n.DefaultLanguage, + } + if store != nil { + if p := store.Get(); p.Language != "" { + l.lang = p.Language + } + } + return l +} + +// Language returns the active language code. +func (l *Localizer) Language() i18n.LanguageCode { + l.mu.RLock() + defer l.mu.RUnlock() + return l.lang +} + +// T resolves key in the current language; args are {placeholder}/value pairs. +// With no bundle wired it returns key unchanged. +func (l *Localizer) T(key string, args ...string) string { + if l == nil || l.bundle == nil { + return key + } + l.mu.RLock() + lang := l.lang + l.mu.RUnlock() + return l.bundle.Translate(lang, key, args...) +} + +// Watch invokes cb on each language change, after the cached language is +// updated so cb may call l.T with the new locale. Replaces any prior subscription. +func (l *Localizer) Watch(cb func(lang i18n.LanguageCode)) { + if l.store == nil { + return + } + ch, unsubscribe := l.store.Subscribe() + l.mu.Lock() + if l.unsubscribe != nil { + l.unsubscribe() + } + l.unsubscribe = unsubscribe + l.mu.Unlock() + + go func() { + for p := range ch { + if p.Language == "" { + continue + } + l.mu.Lock() + if l.lang == p.Language { + l.mu.Unlock() + continue + } + l.lang = p.Language + l.mu.Unlock() + log.Infof("localizer: language switched to %s", p.Language) + if cb != nil { + cb(p.Language) + } + } + }() +} + +// Close cancels the preference subscription. +func (l *Localizer) Close() { + l.mu.Lock() + defer l.mu.Unlock() + if l.unsubscribe != nil { + l.unsubscribe() + l.unsubscribe = nil + } +} + +// StatusLabel maps a daemon status string to its tray label; unrecognised +// statuses pass through verbatim. +func (l *Localizer) StatusLabel(status string) string { + switch { + case status == "", strings.EqualFold(status, services.StatusIdle): + return l.T("tray.status.disconnected") + case strings.EqualFold(status, services.StatusDaemonUnavailable): + return l.T("tray.status.daemonUnavailable") + case strings.EqualFold(status, services.StatusConnected): + return l.T("tray.status.connected") + case strings.EqualFold(status, services.StatusConnecting): + return l.T("tray.status.connecting") + case strings.EqualFold(status, services.StatusNeedsLogin): + return l.T("tray.status.needsLogin") + case strings.EqualFold(status, services.StatusLoginFailed): + return l.T("tray.status.loginFailed") + case strings.EqualFold(status, services.StatusSessionExpired): + return l.T("tray.status.sessionExpired") + } + return status +} diff --git a/client/ui/main.go b/client/ui/main.go new file mode 100644 index 000000000..e6b77762c --- /dev/null +++ b/client/ui/main.go @@ -0,0 +1,387 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "context" + "embed" + "flag" + "io/fs" + "log" + "runtime" + "strings" + + "github.com/sirupsen/logrus" + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" + "github.com/wailsapp/wails/v3/pkg/services/notifications" + + "github.com/netbirdio/netbird/client/ui/authsession" + "github.com/netbirdio/netbird/client/ui/i18n" + "github.com/netbirdio/netbird/client/ui/preferences" + "github.com/netbirdio/netbird/client/ui/services" + "github.com/netbirdio/netbird/client/ui/updater" + "github.com/netbirdio/netbird/util" +) + +//go:embed all:frontend/dist +var assets embed.FS + +// localesRoot embeds the i18n bundles shared by the tray (Go) and the React +// UI (Vite imports the same files). The `all:` prefix is required so +// _index.json is included — //go:embed drops files starting with "_" or "." +// otherwise. +// +//go:embed all:i18n/locales +var localesRoot embed.FS + +// stringList collects repeated string flags. The first user-supplied value +// drops the seeded default; subsequent passes append. +type stringList struct { + values []string + userSet bool +} + +func (s *stringList) String() string { + return strings.Join(s.values, ",") +} + +func (s *stringList) Set(v string) error { + if !s.userSet { + s.values = nil + s.userSet = true + } + s.values = append(s.values, v) + return nil +} + +type registeredServices struct { + connection *services.Connection + authSession *authsession.Session + settings *services.Settings + networks *services.Networks + profiles *services.Profiles + update *services.Update + daemonFeed *services.DaemonFeed + notifier *notifications.NotificationService + compat *services.Compat + profileSwitcher *services.ProfileSwitcher + bundle *i18n.Bundle + prefStore *preferences.Store +} + +func init() { + application.RegisterEvent[services.Status](services.EventStatusSnapshot) + application.RegisterEvent[services.SystemEvent](services.EventDaemonNotification) + application.RegisterEvent[services.ProfileRef](services.EventProfileChanged) + application.RegisterEvent[authsession.Warning](services.EventSessionWarning) + application.RegisterEvent[updater.State](updater.EventStateChanged) + application.RegisterEvent[preferences.UIPreferences](preferences.EventPreferencesChanged) +} + +func main() { + daemonAddr, userSetLogFile := parseFlagsAndInitLog() + conn := NewConn(daemonAddr) + + // Without --log-file, the GUI manages a gui-client.log that follows the + // daemon's debug level and is collected in the debug bundle. It rides + // DaemonFeed's SubscribeEvents stream (see guilog.DebugLog). + debugLog := newDebugLog(userSetLogFile) + + // Declared before app.New so the SingleInstance callback closes over it. + var tray *Tray + app := newApplication(func() { + if tray != nil { + tray.ShowWindow() + } + }) + + settings := services.NewSettings(conn) + profiles := services.NewProfiles(conn) + // updater.Holder owns the typed update State; DaemonFeed feeds it and the + // Update service is a thin Wails-bound facade over it plus the install RPCs. + updaterHolder := updater.NewHolder(app.Event) + update := services.NewUpdate(conn, updaterHolder) + daemonFeed := services.NewDaemonFeed(conn, app.Event, updaterHolder, debugLog) + notifier := notifications.New() + compat := services.NewCompat(conn) + // macOS shows no toast until permission is requested. Run it after + // ApplicationStarted so the notifier's Startup has initialised the + // notification-center delegate. No-op on Linux/Windows (stubs report + // authorized). + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + go requestNotificationAuthorization(notifier) + initDockObserver() + }) + + bundle, prefStore, localizer := buildI18n(app) + + // After bundle + prefStore: both are used to localise daemon errors. + connection := services.NewConnection(conn, bundle, prefStore) + profileSwitcher := services.NewProfileSwitcher(profiles, connection, daemonFeed) + // authsession.Session owns the full extend + dismiss surface the tray + // drives directly; the Wails-bound services.Session wraps only the subset + // the React frontend calls, keeping the generated TS surface minimal. + authSession := authsession.NewSession(conn) + networks := services.NewNetworks(conn) + + registerServices(app, conn, registeredServices{ + connection: connection, + authSession: authSession, + settings: settings, + networks: networks, + profiles: profiles, + update: update, + daemonFeed: daemonFeed, + notifier: notifier, + compat: compat, + profileSwitcher: profileSwitcher, + bundle: bundle, + prefStore: prefStore, + }) + + window := newMainWindow(app, prefStore) + + // Settings is created eagerly (hidden) so the first gear click paints + // instantly and React keeps per-tab state across reopens. The other + // auxiliary windows stay lazy + destroy-on-close so Wails's macOS + // dock-reopen handler can't resurrect them. + windowManager := services.NewWindowManager(app, window, bundle, prefStore, iconWindow) + // Minimal WMs (XEmbed-tray path) neither center small windows nor restore + // position across hide -> show, dropping them top-left. Gate Go-side + // re-centering on that environment; nil leaves placement to the WM on full + // desktops, macOS, and Windows. + windowManager.SetRecenterOnShow(recenterOnShowPredicate()) + app.RegisterService(application.NewService(windowManager)) + + // Welcome window, first launch only — Continue flips OnboardingCompleted + // so later launches skip it. ApplicationStarted hook so the Wails window + // machinery is fully up before the window is created. + if !prefStore.Get().OnboardingCompleted { + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + windowManager.OpenWelcome() + }) + } + + // In-process StatusNotifierWatcher so the tray works on minimal WMs that + // don't ship one (Fluxbox, i3, GNOME without AppIndicator). No-op off + // Linux. Must run before NewTray so the systray's + // RegisterStatusNotifierItem hits a watcher we control. + startStatusNotifierWatcher() + + tray = NewTray(app, window, TrayServices{ + Connection: connection, + Settings: settings, + Profiles: profiles, + Networks: networks, + DaemonFeed: daemonFeed, + Notifier: notifier, + Update: update, + ProfileSwitcher: profileSwitcher, + WindowManager: windowManager, + Session: authSession, + Localizer: localizer, + }) + listenForShowSignal(context.Background(), tray) + + // Start the feed only after every service's ServiceStartup has run. The + // first SubscribeEvents message replays cached state synchronously and can + // fire an OS notification; if Watch ran before app.Run it could beat the + // notifier's ServiceStartup, where the Linux notifier connects the session + // bus — its *dbus.Conn would still be nil and SendNotification would + // nil-deref (fatal panic on the dispatch goroutine, observed on Linux + // Mint). ApplicationStarted fires after the startup loop, so the bus is up. + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + daemonFeed.Watch(context.Background()) + // Probe daemon compatibility once the notifier bus is up; an outdated + // daemon may keep the main window from showing, so the OS toast is the + // only reliable signal the user gets. + go notifyIfDaemonOutdated(compat, notifier, localizer) + }) + + if err := app.Run(); err != nil { + log.Fatal(err) + } +} + +// requestNotificationAuthorization prompts for macOS notification permission. +// The request blocks until the user responds (up to 3 minutes), so callers run +// it in a goroutine. No-op on Linux/Windows. +func requestNotificationAuthorization(notifier *notifications.NotificationService) { + authorized, err := notifier.CheckNotificationAuthorization() + if err != nil { + logrus.Debugf("check notification authorization: %v", err) + return + } + if authorized { + return + } + if _, err := notifier.RequestNotificationAuthorization(); err != nil { + logrus.Debugf("request notification authorization: %v", err) + } +} + +// parseFlagsAndInitLog returns the daemon gRPC address and userSetLogFile +// (true when --log-file was passed). userSetLogFile is the manual-override +// signal: true leaves logging alone, false lets the GUI manage a +// daemon-driven gui-client.log. The flag default is empty (not "console") so +// "no flag" and an explicit "--log-file console" stay distinguishable; empty +// falls back to console for InitLog. +func parseFlagsAndInitLog() (string, bool) { + daemonAddr := flag.String("daemon-addr", DaemonAddr(), "Daemon gRPC address: unix:///path or tcp://host:port") + logFiles := &stringList{} + flag.Var(logFiles, "log-file", "Log destination. Repeat to log to multiple targets at once, e.g. `--log-file console --log-file Y:/netbird-ui.log`. Each value is one of: console, syslog, or a file path. File destinations are rotated by lumberjack (same as the daemon). Defaults to console. Passing any value disables the daemon-debug-driven gui-client.log.") + logLevel := flag.String("log-level", "info", "Log level: trace|debug|info|warn|error.") + flag.Parse() + + userSetLogFile := len(logFiles.values) > 0 + targets := logFiles.values + if !userSetLogFile { + targets = []string{"console"} + } + + if err := util.InitLog(*logLevel, targets...); err != nil { + log.Fatalf("init log: %v", err) + } + return *daemonAddr, userSetLogFile +} + +// newApplication constructs the Wails application. onSecondInstance fires when +// a second process launches under the same SingleInstance UniqueID. +func newApplication(onSecondInstance func()) *application.App { + // On macOS, Options.Icon feeds NSApplication's setApplicationIconImage, + // overriding the bundle icon (Assets.car / icons.icns) the OS already + // picked. Suppress it on darwin to keep the bundle's squircle. + appIcon := iconWindow + if runtime.GOOS == "darwin" { + appIcon = nil + } + + return application.New(application.Options{ + // On Windows, Name is the AppUserModelID for toast notifications and + // the HKCU\Software\Classes\AppUserModelId\ registry path. It must + // match the System.AppUserModel.ID the MSI sets on the Start Menu + // shortcut (client/netbird.wxs) and the AppUserModelId key the + // installer pre-populates with the toast activator CLSID; otherwise + // toasts show under a different identity and the MSI's CustomActivator + // value is orphaned. + Name: "NetBird", + Description: "NetBird desktop client", + Icon: appIcon, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: false, + ActivationPolicy: application.ActivationPolicyAccessory, + }, + Linux: application.LinuxOptions{ + ProgramName: "netbird", + }, + SingleInstance: &application.SingleInstanceOptions{ + UniqueID: "io.netbird.ui", + OnSecondInstanceLaunch: func(_ application.SecondInstanceData) { + onSecondInstance() + }, + }, + }) +} + +// buildI18n constructs the i18n bundle, preferences store, and tray localizer. +// The Bundle satisfies preferences.LanguageValidator so SetLanguage rejects +// codes that have no shipped translation. +func buildI18n(app *application.App) (*i18n.Bundle, *preferences.Store, *Localizer) { + // Reroot the embedded tree at the locales dir so the bundle sees + // _index.json and /common.json at top level (//go:embed roots at + // the package, not the leaf dir). + localesFS, err := fs.Sub(localesRoot, "i18n/locales") + if err != nil { + log.Fatalf("locate locales fs: %v", err) + } + bundle, err := i18n.NewBundle(localesFS) + if err != nil { + log.Fatalf("init i18n bundle: %v", err) + } + prefStore, err := preferences.NewStore(bundle, app.Event) + if err != nil { + log.Fatalf("init preferences store: %v", err) + } + return bundle, prefStore, NewLocalizer(bundle, prefStore) +} + +// registerServices binds every Wails-facing service onto the application. +// Services with no other caller are constructed inline; the rest arrive +// already built so the tray and feed loops share the same instances. +func registerServices(app *application.App, conn *Conn, s registeredServices) { + app.RegisterService(application.NewService(s.connection)) + app.RegisterService(application.NewService(services.NewSession(s.authSession, s.bundle, s.prefStore))) + app.RegisterService(application.NewService(s.settings)) + app.RegisterService(application.NewService(s.networks)) + app.RegisterService(application.NewService(services.NewForwarding(conn))) + app.RegisterService(application.NewService(s.profiles)) + app.RegisterService(application.NewService(services.NewDebug(conn))) + app.RegisterService(application.NewService(s.update)) + app.RegisterService(application.NewService(s.daemonFeed)) + app.RegisterService(application.NewService(s.notifier)) + app.RegisterService(application.NewService(s.profileSwitcher)) + app.RegisterService(application.NewService(services.NewI18n(s.bundle))) + app.RegisterService(application.NewService(services.NewPreferences(s.prefStore))) + app.RegisterService(application.NewService(services.NewAutostart(app.Autostart))) + app.RegisterService(application.NewService(services.NewVersion())) + app.RegisterService(application.NewService(services.NewUILog())) + app.RegisterService(application.NewService(s.compat)) +} + +// newMainWindow creates the hidden main window, sized to the user's last view +// mode, and installs the hide-on-close and macOS dock-reopen hooks. +func newMainWindow(app *application.App, prefStore *preferences.Store) *application.WebviewWindow { + // Width matches the last view mode so Advanced-mode users don't see the + // window pop from 380px to 900px on launch. Height is mode-agnostic. + initialWidth := 380 + if prefStore.Get().ViewMode == preferences.ViewModeAdvanced { + initialWidth = 900 + } + window := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Name: "main", + Title: "NetBird", + Width: initialWidth, + Height: services.WindowHeight, + // Center on first show; minimal WMs (fluxbox, the XEmbed tray path) + // drop new windows top-left unless asked. + InitialPosition: application.WindowCentered, + Hidden: true, + BackgroundColour: services.WindowBackgroundColour, + URL: "/", + DisableResize: true, + MinimiseButtonState: application.ButtonHidden, + MaximiseButtonState: application.ButtonHidden, + Mac: services.AppleMacOSAppearanceOptions(), + Windows: services.MicrosoftWindowsAppearanceOptions(), + Linux: application.LinuxWindow{ + Icon: iconWindow, + }, + }) + + // Hide instead of quit on close; "really quit" is reached via tray -> Quit. + window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { + e.Cancel() + window.Hide() + }) + + // On macOS, Wails' default applicationShouldHandleReopen handler Show()s + // every hidden window on dock-icon click, resurrecting hide-on-close + // surfaces like Settings. Cancel it in a hook (hooks run before listeners) + // and show only the main window. No-op elsewhere — the event never fires. + if runtime.GOOS == "darwin" { + app.Event.RegisterApplicationEventHook(events.Mac.ApplicationShouldHandleReopen, func(e *application.ApplicationEvent) { + e.Cancel() + if e.Context().HasVisibleWindows() { + return + } + window.Show() + window.Focus() + }) + } + + return window +} diff --git a/client/ui/manifest.xml b/client/ui/manifest.xml deleted file mode 100644 index c71a407e5..000000000 --- a/client/ui/manifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - Netbird UI application - - - - - - - - \ No newline at end of file diff --git a/client/ui/netbird-ui.rb.tmpl b/client/ui/netbird-ui.rb.tmpl deleted file mode 100644 index 06971909d..000000000 --- a/client/ui/netbird-ui.rb.tmpl +++ /dev/null @@ -1,39 +0,0 @@ -{{ $projectName := env.Getenv "PROJECT" }}{{ $amdFilePath := env.Getenv "AMD" }}{{ $armFilePath := env.Getenv "ARM" }} -{{ $amdURL := env.Getenv "AMD_URL" }}{{ $armURL := env.Getenv "ARM_URL" }} -{{ $amdFile := filepath.Base $amdFilePath }}{{ $armFile := filepath.Base $armFilePath }}{{ $amdFileBytes := file.Read $amdFilePath }} -{{ $armFileBytes := file.Read $armFilePath }}# Netbird's UI Client Cask Formula -cask "{{ $projectName }}" do - version "{{ env.Getenv "VERSION" }}" - - if Hardware::CPU.intel? - url "{{ $amdURL }}" - sha256 "{{ crypto.SHA256 $amdFileBytes }}" - app "netbird_ui_darwin", target: "Netbird UI.app" - else - url "{{ $armURL }}" - sha256 "{{ crypto.SHA256 $armFileBytes }}" - app "netbird_ui_darwin", target: "Netbird UI.app" - end - - depends_on formula: "netbird" - - postflight do - set_permissions "/Applications/Netbird UI.app/installer.sh", '0755' - set_permissions "/Applications/Netbird UI.app/uninstaller.sh", '0755' - end - - postflight do - system_command "#{appdir}/Netbird UI.app/installer.sh", - args: ["#{version}"], - sudo: true - end - - uninstall_preflight do - system_command "#{appdir}/Netbird UI.app/uninstaller.sh", - sudo: false - end - - name "Netbird UI" - desc "Netbird UI Client" - homepage "https://www.netbird.io/" -end diff --git a/client/ui/network.go b/client/ui/network.go deleted file mode 100644 index cd5d23558..000000000 --- a/client/ui/network.go +++ /dev/null @@ -1,707 +0,0 @@ -//go:build !(linux && 386) - -package main - -import ( - "context" - "fmt" - "runtime" - "sort" - "strings" - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/widget" - "fyne.io/systray" - log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/client/proto" -) - -const ( - allNetworksText = "All networks" - overlappingNetworksText = "Overlapping networks" - exitNodeNetworksText = "Exit-node networks" - allNetworks filter = "all" - overlappingNetworks filter = "overlapping" - exitNodeNetworks filter = "exit-node" - getClientFMT = "get client: %v" -) - -type filter string - -func (s *serviceClient) showNetworksUI() { - s.wNetworks = s.app.NewWindow("Networks") - s.wNetworks.SetOnClosed(s.cancel) - - allGrid := container.New(layout.NewGridLayout(3)) - go s.updateNetworks(allGrid, allNetworks) - overlappingGrid := container.New(layout.NewGridLayout(3)) - exitNodeGrid := container.New(layout.NewGridLayout(3)) - routeCheckContainer := container.NewVBox() - tabs := container.NewAppTabs( - container.NewTabItem(allNetworksText, allGrid), - container.NewTabItem(overlappingNetworksText, overlappingGrid), - container.NewTabItem(exitNodeNetworksText, exitNodeGrid), - ) - tabs.OnSelected = func(item *container.TabItem) { - s.updateNetworksBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodeGrid) - } - tabs.OnUnselected = func(item *container.TabItem) { - grid, _ := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodeGrid) - grid.Objects = nil - } - - routeCheckContainer.Add(tabs) - scrollContainer := container.NewVScroll(routeCheckContainer) - scrollContainer.SetMinSize(fyne.NewSize(200, 300)) - - buttonBox := container.NewHBox( - layout.NewSpacer(), - widget.NewButton("Refresh", func() { - s.updateNetworksBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodeGrid) - }), - widget.NewButton("Select all", func() { - _, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodeGrid) - s.selectAllFilteredNetworks(f) - s.updateNetworksBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodeGrid) - }), - widget.NewButton("Deselect All", func() { - _, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodeGrid) - s.deselectAllFilteredNetworks(f) - s.updateNetworksBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodeGrid) - }), - layout.NewSpacer(), - ) - - content := container.NewBorder(nil, buttonBox, nil, nil, scrollContainer) - - s.wNetworks.SetContent(content) - s.wNetworks.Show() - - s.startAutoRefresh(10*time.Second, tabs, allGrid, overlappingGrid, exitNodeGrid) -} - -func (s *serviceClient) updateNetworks(grid *fyne.Container, f filter) { - grid.Objects = nil - grid.Refresh() - idHeader := widget.NewLabelWithStyle(" ID", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) - networkHeader := widget.NewLabelWithStyle("Range/Domains", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) - resolvedIPsHeader := widget.NewLabelWithStyle("Resolved IPs", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) - - grid.Add(idHeader) - grid.Add(networkHeader) - grid.Add(resolvedIPsHeader) - - filteredRoutes, err := s.getFilteredNetworks(f) - if err != nil { - return - } - - sortNetworksByIDs(filteredRoutes) - - for _, route := range filteredRoutes { - r := route - - checkBox := widget.NewCheck(r.GetID(), func(checked bool) { - s.selectNetwork(r.ID, checked) - }) - checkBox.Checked = route.Selected - checkBox.Resize(fyne.NewSize(20, 20)) - checkBox.Refresh() - - grid.Add(checkBox) - network := r.GetRange() - domains := r.GetDomains() - - if len(domains) == 0 { - grid.Add(widget.NewLabel(network)) - grid.Add(widget.NewLabel("")) - continue - } - - // our selectors are only for display - noopFunc := func(_ string) { - // do nothing - } - - domainsSelector := widget.NewSelect(domains, noopFunc) - domainsSelector.Selected = domains[0] - grid.Add(domainsSelector) - - var resolvedIPsList []string - for domain, ipList := range r.GetResolvedIPs() { - resolvedIPsList = append(resolvedIPsList, fmt.Sprintf("%s: %s", domain, strings.Join(ipList.GetIps(), ", "))) - } - - if len(resolvedIPsList) == 0 { - grid.Add(widget.NewLabel("")) - continue - } - - // TODO: limit width within the selector display - resolvedIPsSelector := widget.NewSelect(resolvedIPsList, noopFunc) - resolvedIPsSelector.Selected = resolvedIPsList[0] - resolvedIPsSelector.Resize(fyne.NewSize(100, 100)) - grid.Add(resolvedIPsSelector) - } - - s.wNetworks.Content().Refresh() - grid.Refresh() -} - -func (s *serviceClient) getFilteredNetworks(f filter) ([]*proto.Network, error) { - routes, err := s.fetchNetworks() - if err != nil { - log.Errorf(getClientFMT, err) - s.showError(fmt.Errorf(getClientFMT, err)) - return nil, err - } - switch f { - case overlappingNetworks: - return getOverlappingNetworks(routes), nil - case exitNodeNetworks: - return getExitNodeNetworks(routes), nil - default: - } - return routes, nil -} - -func getOverlappingNetworks(routes []*proto.Network) []*proto.Network { - var filteredRoutes []*proto.Network - existingRange := make(map[string][]*proto.Network) - for _, route := range routes { - if len(route.Domains) > 0 { - continue - } - if r, exists := existingRange[route.GetRange()]; exists { - r = append(r, route) - existingRange[route.GetRange()] = r - } else { - existingRange[route.GetRange()] = []*proto.Network{route} - } - } - for _, r := range existingRange { - if len(r) > 1 { - filteredRoutes = append(filteredRoutes, r...) - } - } - return filteredRoutes -} - -func isDefaultRoute(routeRange string) bool { - // routeRange is the merged display string from the daemon, e.g. "0.0.0.0/0", - // "::/0", or "0.0.0.0/0, ::/0" when a v4 exit node has a paired v6 entry. - for _, part := range strings.Split(routeRange, ",") { - switch strings.TrimSpace(part) { - case "0.0.0.0/0", "::/0": - return true - } - } - return false -} - -func getExitNodeNetworks(routes []*proto.Network) []*proto.Network { - var filteredRoutes []*proto.Network - for _, route := range routes { - if isDefaultRoute(route.Range) { - filteredRoutes = append(filteredRoutes, route) - } - } - return filteredRoutes -} - -func sortNetworksByIDs(routes []*proto.Network) { - sort.Slice(routes, func(i, j int) bool { - return strings.ToLower(routes[i].GetID()) < strings.ToLower(routes[j].GetID()) - }) -} - -func (s *serviceClient) fetchNetworks() ([]*proto.Network, error) { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return nil, fmt.Errorf(getClientFMT, err) - } - - resp, err := conn.ListNetworks(s.ctx, &proto.ListNetworksRequest{}) - if err != nil { - return nil, fmt.Errorf("failed to list routes: %v", err) - } - - return resp.Routes, nil -} - -func (s *serviceClient) selectNetwork(id string, checked bool) { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - log.Errorf(getClientFMT, err) - s.showError(fmt.Errorf(getClientFMT, err)) - return - } - - req := &proto.SelectNetworksRequest{ - NetworkIDs: []string{id}, - Append: checked, - } - - if checked { - if _, err := conn.SelectNetworks(s.ctx, req); err != nil { - log.Errorf("failed to select network: %v", err) - s.showError(fmt.Errorf("failed to select network: %v", err)) - return - } - log.Infof("Network '%s' selected", id) - } else { - if _, err := conn.DeselectNetworks(s.ctx, req); err != nil { - log.Errorf("failed to deselect network: %v", err) - s.showError(fmt.Errorf("failed to deselect network: %v", err)) - return - } - log.Infof("Network '%s' deselected", id) - } -} - -func (s *serviceClient) selectAllFilteredNetworks(f filter) { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - log.Errorf(getClientFMT, err) - return - } - - req := s.getNetworksRequest(f, true) - if _, err := conn.SelectNetworks(s.ctx, req); err != nil { - log.Errorf("failed to select all networks: %v", err) - s.showError(fmt.Errorf("failed to select all networks: %v", err)) - return - } - - log.Debug("All networks selected") -} - -func (s *serviceClient) deselectAllFilteredNetworks(f filter) { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - log.Errorf(getClientFMT, err) - return - } - - req := s.getNetworksRequest(f, false) - if _, err := conn.DeselectNetworks(s.ctx, req); err != nil { - log.Errorf("failed to deselect all networks: %v", err) - s.showError(fmt.Errorf("failed to deselect all networks: %v", err)) - return - } - - log.Debug("All networks deselected") -} - -func (s *serviceClient) getNetworksRequest(f filter, appendRoute bool) *proto.SelectNetworksRequest { - req := &proto.SelectNetworksRequest{} - if f == allNetworks { - req.All = true - } else { - routes, err := s.getFilteredNetworks(f) - if err != nil { - return nil - } - for _, route := range routes { - req.NetworkIDs = append(req.NetworkIDs, route.GetID()) - } - req.Append = appendRoute - } - return req -} - -func (s *serviceClient) showError(err error) { - wrappedMessage := wrapText(err.Error(), 50) - - dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wNetworks) -} - -func (s *serviceClient) startAutoRefresh(interval time.Duration, tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) { - ticker := time.NewTicker(interval) - go func() { - for range ticker.C { - s.updateNetworksBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodesGrid) - } - }() - - s.wNetworks.SetOnClosed(func() { - ticker.Stop() - s.cancel() - }) -} - -func (s *serviceClient) updateNetworksBasedOnDisplayTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) { - grid, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodesGrid) - s.wNetworks.Content().Refresh() - s.updateNetworks(grid, f) -} - -// startExitNodeRefresh initiates exit node menu refresh after connecting. -// On Windows, TrayOpenedCh is not supported by the systray library, so we use -// a background poller to keep exit nodes in sync while connected. -// On macOS/Linux, TrayOpenedCh handles refreshes on each tray open. -func (s *serviceClient) startExitNodeRefresh() { - s.cancelExitNodeRetry() - - if runtime.GOOS == "windows" { - ctx, cancel := context.WithCancel(s.ctx) - s.exitNodeMu.Lock() - s.exitNodeRetryCancel = cancel - s.exitNodeMu.Unlock() - - go s.pollExitNodes(ctx) - } else { - go s.updateExitNodes() - } -} - -func (s *serviceClient) cancelExitNodeRetry() { - s.exitNodeMu.Lock() - if s.exitNodeRetryCancel != nil { - s.exitNodeRetryCancel() - s.exitNodeRetryCancel = nil - } - s.exitNodeMu.Unlock() -} - -// pollExitNodes periodically refreshes exit nodes while connected. -// Uses a short initial interval to catch routes from the management sync, -// then switches to a longer interval for ongoing updates. -func (s *serviceClient) pollExitNodes(ctx context.Context) { - // Initial fast polling to catch routes as they appear after connect. - for i := 0; i < 5; i++ { - if s.updateExitNodes() { - break - } - select { - case <-ctx.Done(): - return - case <-time.After(2 * time.Second): - } - } - - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - s.updateExitNodes() - } - } -} - -// updateExitNodes fetches exit nodes from the daemon and recreates the menu. -// Returns true if exit nodes were found. -func (s *serviceClient) updateExitNodes() bool { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - log.Errorf("get client: %v", err) - return false - } - exitNodes, err := s.getExitNodes(conn) - if err != nil { - log.Errorf("get exit nodes: %v", err) - return false - } - - s.exitNodeMu.Lock() - defer s.exitNodeMu.Unlock() - - s.recreateExitNodeMenu(exitNodes) - - if len(s.mExitNodeItems) > 0 { - s.mExitNode.Enable() - return true - } - - s.mExitNode.Disable() - return false -} - -func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { - for _, node := range s.mExitNodeItems { - node.cancel() - node.Hide() - node.Remove() - } - s.mExitNodeItems = nil - if s.mExitNodeSeparator != nil { - s.mExitNodeSeparator.Remove() - s.mExitNodeSeparator = nil - } - if s.mExitNodeDeselectAll != nil { - s.mExitNodeDeselectAll.Remove() - s.mExitNodeDeselectAll = nil - } - - if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { - s.mExitNode.Remove() - s.mExitNode = systray.AddMenuItem("Exit Node", disabledMenuDescr) - } - - var showDeselectAll bool - - for _, node := range exitNodes { - if node.Selected { - showDeselectAll = true - } - - menuItem := s.mExitNode.AddSubMenuItemCheckbox( - node.ID, - fmt.Sprintf("Use exit node %s", node.ID), - node.Selected, - ) - - ctx, cancel := context.WithCancel(s.ctx) - s.mExitNodeItems = append(s.mExitNodeItems, menuHandler{ - MenuItem: menuItem, - cancel: cancel, - }) - go s.handleChecked(ctx, node.ID, menuItem) - } - - if showDeselectAll { - s.addExitNodeDeselectAll() - } - -} - -func (s *serviceClient) addExitNodeDeselectAll() { - sep := s.mExitNode.AddSubMenuItem("───────────────", "") - sep.Disable() - s.mExitNodeSeparator = sep - - deselectAllItem := s.mExitNode.AddSubMenuItem("Deselect All", "Deselect All") - s.mExitNodeDeselectAll = deselectAllItem - - go func() { - for { - _, ok := <-deselectAllItem.ClickedCh - if !ok { - return - } - exitNodes, err := s.handleExitNodeMenuDeselectAll() - if err != nil { - log.Warnf("failed to handle deselect all exit nodes: %v", err) - } else { - s.exitNodeMu.Lock() - s.recreateExitNodeMenu(exitNodes) - s.exitNodeMu.Unlock() - } - } - }() -} - -func (s *serviceClient) getExitNodes(conn proto.DaemonServiceClient) ([]*proto.Network, error) { - ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) - defer cancel() - - resp, err := conn.ListNetworks(ctx, &proto.ListNetworksRequest{}) - if err != nil { - return nil, fmt.Errorf("list networks: %v", err) - } - - var exitNodes []*proto.Network - for _, network := range resp.Routes { - if isDefaultRoute(network.Range) { - exitNodes = append(exitNodes, network) - } - } - return exitNodes, nil -} - -func (s *serviceClient) handleChecked(ctx context.Context, id string, item *systray.MenuItem) { - for { - select { - case <-ctx.Done(): - return - case _, ok := <-item.ClickedCh: - if !ok { - return - } - if err := s.toggleExitNode(id, item); err != nil { - log.Errorf("failed to toggle exit node: %v", err) - continue - } - } - } -} - -func (s *serviceClient) handleExitNodeMenuDeselectAll() ([]*proto.Network, error) { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return nil, fmt.Errorf("get client: %v", err) - } - - exitNodes, err := s.getExitNodes(conn) - if err != nil { - return nil, fmt.Errorf("get exit nodes: %v", err) - } - - var ids []string - for _, e := range exitNodes { - if e.Selected { - ids = append(ids, e.ID) - } - } - - // deselect selected exit nodes - if err := s.deselectOtherExitNodes(conn, ids); err != nil { - return nil, err - } - - updatedExitNodes, err := s.getExitNodes(conn) - if err != nil { - return nil, fmt.Errorf("re-fetch exit nodes: %v", err) - } - - return updatedExitNodes, nil -} - -// Add function to toggle exit node selection -func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) error { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return fmt.Errorf("get client: %v", err) - } - - log.Infof("Toggling exit node '%s'", nodeID) - - s.exitNodeMu.Lock() - defer s.exitNodeMu.Unlock() - - exitNodes, err := s.getExitNodes(conn) - if err != nil { - return fmt.Errorf("get exit nodes: %v", err) - } - - var exitNode *proto.Network - // find other selected nodes and ours - ids := make([]string, 0, len(exitNodes)) - for _, node := range exitNodes { - if node.ID == nodeID { - // preserve original state - cp := *node //nolint:govet - exitNode = &cp - - // set desired state for recreation - node.Selected = true - continue - } - if node.Selected { - ids = append(ids, node.ID) - - // set desired state for recreation - node.Selected = false - } - } - - // exit node is the only selected node, deselect it - deselectAll := item.Checked() && len(ids) == 0 - if deselectAll { - ids = append(ids, nodeID) - for _, node := range exitNodes { - if node.ID == nodeID { - // set desired state for recreation - node.Selected = false - } - } - } - - // deselect all other selected exit nodes - if err := s.deselectOtherExitNodes(conn, ids); err != nil { - return err - } - - if !deselectAll { - if err := s.selectNewExitNode(conn, exitNode, nodeID, item); err != nil { - return err - } - } - - // linux/bsd doesn't handle Check/Uncheck well, so we recreate the menu - if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { - s.recreateExitNodeMenu(exitNodes) - } - - return nil -} - -func (s *serviceClient) deselectOtherExitNodes(conn proto.DaemonServiceClient, ids []string) error { - // deselect all other selected exit nodes - if len(ids) > 0 { - deselectReq := &proto.SelectNetworksRequest{ - NetworkIDs: ids, - } - if _, err := conn.DeselectNetworks(s.ctx, deselectReq); err != nil { - return fmt.Errorf("deselect networks: %v", err) - } - - log.Infof("Deselected exit nodes: %v", ids) - } - - // uncheck all other exit node menu items - for _, i := range s.mExitNodeItems { - i.Uncheck() - log.Infof("Unchecked exit node %v", i) - } - - return nil -} - -func (s *serviceClient) selectNewExitNode(conn proto.DaemonServiceClient, exitNode *proto.Network, nodeID string, item *systray.MenuItem) error { - if exitNode != nil && !exitNode.Selected { - selectReq := &proto.SelectNetworksRequest{ - NetworkIDs: []string{exitNode.ID}, - Append: true, - } - if _, err := conn.SelectNetworks(s.ctx, selectReq); err != nil { - return fmt.Errorf("select network: %v", err) - } - - log.Infof("Selected exit node '%s'", nodeID) - } - - item.Check() - log.Infof("Checked exit node '%s'", nodeID) - - return nil -} - -func getGridAndFilterFromTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) (*fyne.Container, filter) { - switch tabs.Selected().Text { - case overlappingNetworksText: - return overlappingGrid, overlappingNetworks - case exitNodeNetworksText: - return exitNodesGrid, exitNodeNetworks - default: - return allGrid, allNetworks - } -} - -// wrapText inserts newlines into the text to ensure that each line is -// no longer than 'lineLength' runes. -func wrapText(text string, lineLength int) string { - var sb strings.Builder - var currentLineLength int - - for _, runeValue := range text { - sb.WriteRune(runeValue) - currentLineLength++ - - if currentLineLength >= lineLength || runeValue == '\n' { - sb.WriteRune('\n') - currentLineLength = 0 - } - } - - return sb.String() -} diff --git a/client/ui/notifier/notifier.go b/client/ui/notifier/notifier.go deleted file mode 100644 index 8d1cbe4c4..000000000 --- a/client/ui/notifier/notifier.go +++ /dev/null @@ -1,27 +0,0 @@ -// Package notifier sends desktop notifications. On Windows it uses the WinRT -// COM API directly via go-toast/v2 to avoid the PowerShell window flash that -// fyne's default implementation produces. On other platforms it delegates to -// fyne. -package notifier - -import "fyne.io/fyne/v2" - -// Notifier sends desktop notifications. -type Notifier interface { - Send(title, body string) -} - -// New returns a platform-specific Notifier. The fyne app is used as the -// fallback notifier on platforms where no native implementation is wired up, -// and on Windows when the COM path fails to initialize. -func New(app fyne.App) Notifier { - return newNotifier(app) -} - -type fyneNotifier struct { - app fyne.App -} - -func (f *fyneNotifier) Send(title, body string) { - f.app.SendNotification(fyne.NewNotification(title, body)) -} diff --git a/client/ui/notifier/notifier_other.go b/client/ui/notifier/notifier_other.go deleted file mode 100644 index 686d2885f..000000000 --- a/client/ui/notifier/notifier_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !windows - -package notifier - -import "fyne.io/fyne/v2" - -func newNotifier(app fyne.App) Notifier { - return &fyneNotifier{app: app} -} diff --git a/client/ui/notifier/notifier_windows.go b/client/ui/notifier/notifier_windows.go deleted file mode 100644 index c7afb43ae..000000000 --- a/client/ui/notifier/notifier_windows.go +++ /dev/null @@ -1,88 +0,0 @@ -package notifier - -import ( - "os" - "path/filepath" - "sync" - - "fyne.io/fyne/v2" - toast "git.sr.ht/~jackmordaunt/go-toast/v2" - "git.sr.ht/~jackmordaunt/go-toast/v2/wintoast" - log "github.com/sirupsen/logrus" -) - -const ( - // appID is the AppUserModelID shown in the Windows Action Center. It - // must match the System.AppUserModel.ID property set on the Start Menu - // shortcut by the MSI (see client/netbird.wxs); otherwise Windows - // groups toasts under a separate, unbranded entry. - appID = "NetBird" - - // appGUID identifies the COM activation callback class. Generated once - // for NetBird; do not change without coordinating an installer bump, - // since old registry entries pointing at the previous GUID would orphan. - appGUID = "{0E1B4DE7-E148-432B-9814-544F941826EC}" -) - -type comNotifier struct { - fallback *fyneNotifier - ready bool - iconPath string -} - -var ( - initOnce sync.Once - initErr error -) - -func newNotifier(app fyne.App) Notifier { - n := &comNotifier{ - fallback: &fyneNotifier{app: app}, - iconPath: resolveIcon(), - } - initOnce.Do(func() { - initErr = wintoast.SetAppData(wintoast.AppData{ - AppID: appID, - GUID: appGUID, - IconPath: n.iconPath, - }) - }) - if initErr != nil { - log.Warnf("toast: register app data failed, falling back to fyne notifications: %v", initErr) - return n.fallback - } - n.ready = true - return n -} - -func (n *comNotifier) Send(title, body string) { - if !n.ready { - n.fallback.Send(title, body) - return - } - notification := toast.Notification{ - AppID: appID, - Title: title, - Body: body, - Icon: n.iconPath, - } - if err := notification.Push(); err != nil { - log.Warnf("toast: push failed, using fyne fallback: %v", err) - n.fallback.Send(title, body) - } -} - -// resolveIcon returns an absolute path to the toast icon, or an empty string -// when no icon can be located. Windows requires a PNG/JPG for the -// AppUserModelId IconUri registry value; .ico is silently ignored. -func resolveIcon() string { - exe, err := os.Executable() - if err != nil { - return "" - } - candidate := filepath.Join(filepath.Dir(exe), "netbird.png") - if _, err := os.Stat(candidate); err == nil { - return candidate - } - return "" -} diff --git a/client/ui/preferences/store.go b/client/ui/preferences/store.go new file mode 100644 index 000000000..afc854185 --- /dev/null +++ b/client/ui/preferences/store.go @@ -0,0 +1,267 @@ +//go:build !android && !ios && !freebsd && !js + +// Package preferences holds user-scope UI state, independent of the daemon +// profile and shared across all profiles. The Store persists to JSON under +// os.UserConfigDir() and broadcasts changes to in-process subscribers plus an +// optional emitter. +package preferences + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/ui/i18n" + "github.com/netbirdio/netbird/util" +) + +// Lives under os.UserConfigDir()/netbird (OS-user writable, not the daemon's +// root-owned state). +const preferencesFileName = "ui-preferences.json" + +// EventPreferencesChanged fires on every persisted update, payload UIPreferences. +const EventPreferencesChanged = "netbird:preferences:changed" + +// ViewMode is the preferred Main-window layout: "default" (compact, 380-wide) +// or "advanced" (900-wide). +type ViewMode string + +const ( + ViewModeDefault ViewMode = "default" + ViewModeAdvanced ViewMode = "advanced" +) + +// DefaultViewMode applies when no file exists or its view-mode is empty. +const DefaultViewMode = ViewModeDefault + +var ErrUnsupportedViewMode = errors.New("unsupported view mode") + +func (v ViewMode) IsValid() bool { + switch v { + case ViewModeDefault, ViewModeAdvanced: + return true + } + return false +} + +// UIPreferences is rewritten in full on every change; there are no partial updates. +type UIPreferences struct { + Language i18n.LanguageCode `json:"language"` + ViewMode ViewMode `json:"viewMode"` + OnboardingCompleted bool `json:"onboardingCompleted"` +} + +// LanguageValidator rejects SetLanguage inputs with no shipped bundle. +// *i18n.Bundle satisfies it. +type LanguageValidator interface { + HasLanguage(code i18n.LanguageCode) bool +} + +// Emitter broadcasts changes to the frontend. Wails' +// *application.EventProcessor satisfies it; tests pass nil or a fake. +type Emitter interface { + Emit(name string, data ...any) bool +} + +// Store is the user-scope UI preferences store. +type Store struct { + path string + + mu sync.RWMutex + current UIPreferences + + subsMu sync.Mutex + subs []chan UIPreferences + + validator LanguageValidator + emitter Emitter +} + +// NewStore loads preferences from disk, falling back to defaults. A nil +// validator skips SetLanguage validation; a nil emitter skips broadcasting. +func NewStore(validator LanguageValidator, emitter Emitter) (*Store, error) { + path, err := preferencesPath() + if err != nil { + return nil, fmt.Errorf("resolve preferences path: %w", err) + } + + // Language starts empty: the frontend treats absence as the signal to + // detect the browser locale on first launch and call SetLanguage. + s := &Store{ + path: path, + validator: validator, + emitter: emitter, + current: UIPreferences{ViewMode: DefaultViewMode}, + } + + if err := s.load(); err != nil { + log.Warnf("load ui preferences from %s: %v (using defaults)", path, err) + } + + return s, nil +} + +// Get returns a copy of the current preferences. +func (s *Store) Get() UIPreferences { + s.mu.RLock() + defer s.mu.RUnlock() + return s.current +} + +// SetViewMode validates, persists, and broadcasts. No-op if unchanged. +func (s *Store) SetViewMode(mode ViewMode) error { + if !mode.IsValid() { + return fmt.Errorf("%w: %q", ErrUnsupportedViewMode, mode) + } + + s.mu.Lock() + if s.current.ViewMode == mode { + s.mu.Unlock() + return nil + } + next := s.current + next.ViewMode = mode + if err := s.persistLocked(next); err != nil { + s.mu.Unlock() + return fmt.Errorf("persist preferences: %w", err) + } + s.current = next + s.mu.Unlock() + + s.broadcast(next) + return nil +} + +// SetOnboardingCompleted persists the welcome-window dismissal. No-op if unchanged. +func (s *Store) SetOnboardingCompleted(done bool) error { + s.mu.Lock() + if s.current.OnboardingCompleted == done { + s.mu.Unlock() + return nil + } + next := s.current + next.OnboardingCompleted = done + if err := s.persistLocked(next); err != nil { + s.mu.Unlock() + return fmt.Errorf("persist preferences: %w", err) + } + s.current = next + s.mu.Unlock() + + s.broadcast(next) + return nil +} + +// SetLanguage validates, persists, and broadcasts. No-op if unchanged. +func (s *Store) SetLanguage(lang i18n.LanguageCode) error { + if lang == "" { + return fmt.Errorf("%w: empty code", i18n.ErrUnsupportedLanguage) + } + if s.validator != nil && !s.validator.HasLanguage(lang) { + return fmt.Errorf("%w: %q", i18n.ErrUnsupportedLanguage, lang) + } + + s.mu.Lock() + if s.current.Language == lang { + s.mu.Unlock() + return nil + } + next := s.current + next.Language = lang + if err := s.persistLocked(next); err != nil { + s.mu.Unlock() + return fmt.Errorf("persist preferences: %w", err) + } + s.current = next + s.mu.Unlock() + + s.broadcast(next) + return nil +} + +// Subscribe returns a channel of persisted changes and an unsubscribe func. +// The unsubscribe func closes the channel; callers must not close it themselves. +func (s *Store) Subscribe() (<-chan UIPreferences, func()) { + ch := make(chan UIPreferences, 4) + s.subsMu.Lock() + s.subs = append(s.subs, ch) + s.subsMu.Unlock() + + unsubscribe := func() { + s.subsMu.Lock() + defer s.subsMu.Unlock() + for i, c := range s.subs { + if c == ch { + s.subs = append(s.subs[:i], s.subs[i+1:]...) + close(ch) + return + } + } + } + return ch, unsubscribe +} + +// load reads the file into current. A missing file is not an error (the +// in-memory default stands); malformed contents return an error. +func (s *Store) load() error { + if _, err := os.Stat(s.path); errors.Is(err, os.ErrNotExist) { + return nil + } + + var loaded UIPreferences + if _, err := util.ReadJson(s.path, &loaded); err != nil { + return err + } + + if !loaded.ViewMode.IsValid() { + loaded.ViewMode = DefaultViewMode + } + + s.mu.Lock() + s.current = loaded + s.mu.Unlock() + return nil +} + +// persistLocked writes v to disk. Caller must hold s.mu and update in-memory +// state only after this returns nil. +func (s *Store) persistLocked(v UIPreferences) error { + if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { + return fmt.Errorf("mkdir %s: %w", filepath.Dir(s.path), err) + } + return util.WriteJson(context.Background(), s.path, v) +} + +// broadcast fans v out to subscribers and the emitter. Full-buffer subscribers +// are skipped: consumers only need the latest value, so dropping is safe. +func (s *Store) broadcast(v UIPreferences) { + s.subsMu.Lock() + subs := make([]chan UIPreferences, len(s.subs)) + copy(subs, s.subs) + s.subsMu.Unlock() + + for _, ch := range subs { + select { + case ch <- v: + default: + log.Debugf("preferences subscriber channel full; dropping update") + } + } + + if s.emitter != nil { + s.emitter.Emit(EventPreferencesChanged, v) + } +} + +func preferencesPath() (string, error) { + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "netbird", preferencesFileName), nil +} diff --git a/client/ui/preferences/store_test.go b/client/ui/preferences/store_test.go new file mode 100644 index 000000000..0d1cc7b54 --- /dev/null +++ b/client/ui/preferences/store_test.go @@ -0,0 +1,225 @@ +//go:build !android && !ios && !freebsd && !js + +package preferences + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "runtime" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/ui/i18n" +) + +// fakeValidator implements LanguageValidator for tests so we don't need a +// fully-loaded i18n.Bundle. +type fakeValidator struct{ ok map[i18n.LanguageCode]bool } + +func (f fakeValidator) HasLanguage(code i18n.LanguageCode) bool { return f.ok[code] } + +// recordingEmitter captures Emit calls so tests can assert the broadcast +// fired. +type recordingEmitter struct { + mu sync.Mutex + calls []emitCall +} + +type emitCall struct { + name string + data []any +} + +func (r *recordingEmitter) Emit(name string, data ...any) bool { + r.mu.Lock() + defer r.mu.Unlock() + r.calls = append(r.calls, emitCall{name: name, data: data}) + return true +} + +func (r *recordingEmitter) calledWith(name string) []emitCall { + r.mu.Lock() + defer r.mu.Unlock() + var out []emitCall + for _, c := range r.calls { + if c.name == name { + out = append(out, c) + } + } + return out +} + +// withTempConfigDir reroots os.UserConfigDir() at a temporary directory by +// pointing the OS-specific env vars there. Restored automatically by +// t.Setenv. +func withTempConfigDir(t *testing.T) string { + t.Helper() + tmp := t.TempDir() + switch runtime.GOOS { + case "darwin": + t.Setenv("HOME", tmp) + require.NoError(t, os.MkdirAll(filepath.Join(tmp, "Library", "Application Support"), 0o755)) + case "windows": + t.Setenv("AppData", tmp) + default: + t.Setenv("XDG_CONFIG_HOME", tmp) + } + return tmp +} + +func TestStore_DefaultsWhenFileMissing(t *testing.T) { + withTempConfigDir(t) + s, err := NewStore(fakeValidator{ok: map[i18n.LanguageCode]bool{"en": true}}, nil) + require.NoError(t, err) + + got := s.Get() + assert.Equal(t, i18n.LanguageCode(""), got.Language, "language must be empty when no file is on disk so the frontend can detect the browser locale") + assert.Equal(t, DefaultViewMode, got.ViewMode, "view-mode default should still apply") +} + +func TestStore_SetLanguagePersistsAndBroadcasts(t *testing.T) { + withTempConfigDir(t) + emitter := &recordingEmitter{} + s, err := NewStore(fakeValidator{ok: map[i18n.LanguageCode]bool{"en": true, "hu": true}}, emitter) + require.NoError(t, err) + + ch, unsubscribe := s.Subscribe() + defer unsubscribe() + + require.NoError(t, s.SetLanguage("hu")) + + got := s.Get() + assert.Equal(t, i18n.LanguageCode("hu"), got.Language, "Get should reflect the SetLanguage value") + + select { + case v := <-ch: + assert.Equal(t, i18n.LanguageCode("hu"), v.Language, "subscriber should receive the new value") + case <-time.After(time.Second): + t.Fatal("subscriber timed out waiting for update") + } + + emits := emitter.calledWith(EventPreferencesChanged) + require.Len(t, emits, 1, "Emit should fire exactly once per SetLanguage") + payload, ok := emits[0].data[0].(UIPreferences) + require.True(t, ok, "emitter payload should be UIPreferences") + assert.Equal(t, i18n.LanguageCode("hu"), payload.Language) +} + +func TestStore_LoadFromDisk(t *testing.T) { + withTempConfigDir(t) + path, err := preferencesPath() + require.NoError(t, err) + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(`{"language":"hu"}`), 0o644)) + + s, err := NewStore(fakeValidator{ok: map[i18n.LanguageCode]bool{"hu": true}}, nil) + require.NoError(t, err) + + got := s.Get() + assert.Equal(t, i18n.LanguageCode("hu"), got.Language, "Get should load language from existing file") +} + +func TestStore_UnsupportedLanguageRejected(t *testing.T) { + withTempConfigDir(t) + s, err := NewStore(fakeValidator{ok: map[i18n.LanguageCode]bool{"en": true}}, nil) + require.NoError(t, err) + + err = s.SetLanguage("xx") + require.Error(t, err, "unknown language must be rejected") + assert.ErrorIs(t, err, i18n.ErrUnsupportedLanguage) + + err = s.SetLanguage("") + assert.ErrorIs(t, err, i18n.ErrUnsupportedLanguage, "empty language code must be rejected") +} + +func TestStore_NoValidatorAcceptsAnything(t *testing.T) { + withTempConfigDir(t) + s, err := NewStore(nil, nil) + require.NoError(t, err) + + require.NoError(t, s.SetLanguage("fr")) + got := s.Get() + assert.Equal(t, i18n.LanguageCode("fr"), got.Language) +} + +func TestStore_SetLanguageIdempotent(t *testing.T) { + withTempConfigDir(t) + emitter := &recordingEmitter{} + s, err := NewStore(fakeValidator{ok: map[i18n.LanguageCode]bool{"en": true}}, emitter) + require.NoError(t, err) + + // First call goes from "" (unset) to "en" — real change, one broadcast. + require.NoError(t, s.SetLanguage("en")) + require.Len(t, emitter.calledWith(EventPreferencesChanged), 1, + "first SetLanguage from unset should broadcast") + + // Second call is a no-op — no disk write, no broadcast. Without this + // guard the tray would re-render the menu on every cosmetic re-save of + // the preferences file. + require.NoError(t, s.SetLanguage("en")) + assert.Len(t, emitter.calledWith(EventPreferencesChanged), 1, + "re-setting the current language should not broadcast again") +} + +func TestStore_CorruptFileFallsBackToDefault(t *testing.T) { + withTempConfigDir(t) + path, err := preferencesPath() + require.NoError(t, err) + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte("{not json"), 0o644)) + + s, err := NewStore(fakeValidator{ok: map[i18n.LanguageCode]bool{"en": true}}, nil) + require.NoError(t, err, "corrupt file should not fail construction") + + got := s.Get() + assert.Equal(t, i18n.LanguageCode(""), got.Language, "corrupt JSON should leave the empty (unset) default in place so the frontend can re-detect") +} + +func TestStore_UnsubscribeStopsUpdates(t *testing.T) { + withTempConfigDir(t) + s, err := NewStore(fakeValidator{ok: map[i18n.LanguageCode]bool{"en": true, "hu": true}}, nil) + require.NoError(t, err) + + ch, unsubscribe := s.Subscribe() + unsubscribe() + + require.NoError(t, s.SetLanguage("hu")) + + select { + case _, ok := <-ch: + assert.False(t, ok, "channel should be closed after unsubscribe") + case <-time.After(time.Second): + t.Fatal("expected closed channel, got nothing") + } +} + +func TestStore_FileShapeIsJSON(t *testing.T) { + withTempConfigDir(t) + s, err := NewStore(fakeValidator{ok: map[i18n.LanguageCode]bool{"hu": true}}, nil) + require.NoError(t, err) + require.NoError(t, s.SetLanguage("hu")) + + path, err := preferencesPath() + require.NoError(t, err) + data, err := os.ReadFile(path) + require.NoError(t, err) + + var parsed UIPreferences + require.NoError(t, json.Unmarshal(data, &parsed), "on-disk file must be valid JSON") + assert.Equal(t, i18n.LanguageCode("hu"), parsed.Language) +} + +func TestStore_ErrUnsupportedSentinel(t *testing.T) { + // Verifies callers can match on the sentinel error rather than parsing + // strings — protects against accidental %v -> %w changes that would + // silently break errors.Is. + err := errors.New("inner") + wrapped := errors.Join(i18n.ErrUnsupportedLanguage, err) + assert.ErrorIs(t, wrapped, i18n.ErrUnsupportedLanguage) +} diff --git a/client/ui/process/process.go b/client/ui/process/process.go deleted file mode 100644 index 28276f416..000000000 --- a/client/ui/process/process.go +++ /dev/null @@ -1,38 +0,0 @@ -package process - -import ( - "os" - "path/filepath" - "strings" - - "github.com/shirou/gopsutil/v3/process" -) - -func IsAnotherProcessRunning() (int32, bool, error) { - processes, err := process.Processes() - if err != nil { - return 0, false, err - } - - pid := os.Getpid() - processName := strings.ToLower(filepath.Base(os.Args[0])) - - for _, p := range processes { - if int(p.Pid) == pid { - continue - } - - runningProcessPath, err := p.Exe() - // most errors are related to short-lived processes - if err != nil { - continue - } - - runningProcessName := strings.ToLower(filepath.Base(runningProcessPath)) - if runningProcessName == processName && isProcessOwnedByCurrentUser(p) { - return p.Pid, true, nil - } - } - - return 0, false, nil -} diff --git a/client/ui/process/process_nonwindows.go b/client/ui/process/process_nonwindows.go deleted file mode 100644 index cf9f6443d..000000000 --- a/client/ui/process/process_nonwindows.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !windows - -package process - -import ( - "os" - - "github.com/shirou/gopsutil/v3/process" - log "github.com/sirupsen/logrus" -) - -func isProcessOwnedByCurrentUser(p *process.Process) bool { - currentUserID := os.Getuid() - uids, err := p.Uids() - if err != nil { - log.Errorf("get process uids: %v", err) - return false - } - for _, id := range uids { - log.Debugf("checking process uid: %d", id) - if int(id) == currentUserID { - return true - } - } - return false -} diff --git a/client/ui/process/process_windows.go b/client/ui/process/process_windows.go deleted file mode 100644 index 2d211d1a4..000000000 --- a/client/ui/process/process_windows.go +++ /dev/null @@ -1,24 +0,0 @@ -package process - -import ( - "os/user" - - "github.com/shirou/gopsutil/v3/process" - log "github.com/sirupsen/logrus" -) - -func isProcessOwnedByCurrentUser(p *process.Process) bool { - processUsername, err := p.Username() - if err != nil { - log.Errorf("get process username error: %v", err) - return false - } - - currUser, err := user.Current() - if err != nil { - log.Errorf("get current user error: %v", err) - return false - } - - return processUsername == currUser.Username -} diff --git a/client/ui/profile.go b/client/ui/profile.go deleted file mode 100644 index 83b0ec18b..000000000 --- a/client/ui/profile.go +++ /dev/null @@ -1,775 +0,0 @@ -//go:build !(linux && 386) - -package main - -import ( - "context" - "errors" - "fmt" - "os/user" - "slices" - "sort" - "sync" - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/widget" - "fyne.io/systray" - log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/internal/profilemanager" - "github.com/netbirdio/netbird/client/proto" -) - -// showProfilesUI creates and displays the Profiles window with a list of existing profiles, -// a button to add new profiles, allows removal, and lets the user switch the active profile. -func (s *serviceClient) showProfilesUI() { - - profiles, err := s.getProfiles() - if err != nil { - log.Errorf("get profiles: %v", err) - return - } - - var refresh func() - // List widget for profiles - list := widget.NewList( - func() int { return len(profiles) }, - func() fyne.CanvasObject { - // Each item: Selected indicator, Name, spacer, Select, Logout & Remove buttons - return container.NewHBox( - widget.NewLabel(""), // indicator - widget.NewLabel(""), // profile name - layout.NewSpacer(), - widget.NewButton("Select", nil), - widget.NewButton("Deregister", nil), - widget.NewButton("Remove", nil), - ) - }, - func(i widget.ListItemID, item fyne.CanvasObject) { - // Populate each row - row := item.(*fyne.Container) - indicator := row.Objects[0].(*widget.Label) - nameLabel := row.Objects[1].(*widget.Label) - selectBtn := row.Objects[3].(*widget.Button) - logoutBtn := row.Objects[4].(*widget.Button) - removeBtn := row.Objects[5].(*widget.Button) - - profile := profiles[i] - // Show a checkmark if selected - if profile.IsActive { - indicator.SetText("✓") - } else { - indicator.SetText("") - } - nameLabel.SetText(formatProfileLabel(profile, profiles)) - - // Configure Select/Active button - selectBtn.SetText(func() string { - if profile.IsActive { - return "Active" - } - return "Select" - }()) - selectBtn.OnTapped = func() { - if profile.IsActive { - return // already active - } - // confirm switch - dialog.ShowConfirm( - "Switch Profile", - fmt.Sprintf("Are you sure you want to switch to '%s'?", profile.Name), - func(confirm bool) { - if !confirm { - return - } - // switch - err = s.switchProfile(profile.ID) - if err != nil { - log.Errorf("failed to switch profile: %v", err) - dialog.ShowError(errors.New("failed to select profile"), s.wProfiles) - return - } - - dialog.ShowInformation( - "Profile Switched", - fmt.Sprintf("Profile '%s' switched successfully", profile.Name), - s.wProfiles, - ) - - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - log.Errorf("failed to get daemon client: %v", err) - return - } - - status, err := conn.Status(s.ctx, &proto.StatusRequest{}) - if err != nil { - log.Errorf("failed to get status after switching profile: %v", err) - return - } - - if status.Status == string(internal.StatusConnected) { - if err := s.menuDownClick(); err != nil { - log.Errorf("failed to handle down click after switching profile: %v", err) - dialog.ShowError(fmt.Errorf("failed to handle down click"), s.wProfiles) - return - } - } - // update slice flags - refresh() - }, - s.wProfiles, - ) - } - - logoutBtn.Show() - logoutBtn.SetText("Deregister") - logoutBtn.OnTapped = func() { - s.handleProfileLogout(profile, refresh) - } - - // Remove profile - removeBtn.SetText("Remove") - removeBtn.OnTapped = func() { - dialog.ShowConfirm( - "Delete Profile", - fmt.Sprintf("Are you sure you want to delete '%s'?", profile.Name), - func(confirm bool) { - if !confirm { - return - } - - err = s.removeProfile(profile.ID) - if err != nil { - log.Errorf("failed to remove profile: %v", err) - dialog.ShowError(fmt.Errorf("failed to remove profile"), s.wProfiles) - return - } - dialog.ShowInformation( - "Profile Removed", - fmt.Sprintf("Profile '%s' removed successfully", profile.Name), - s.wProfiles, - ) - // update slice - refresh() - }, - s.wProfiles, - ) - } - }, - ) - - refresh = func() { - newProfiles, err := s.getProfiles() - if err != nil { - dialog.ShowError(err, s.wProfiles) - return - } - profiles = newProfiles // update the slice - list.Refresh() // tell Fyne to re-call length/update on every visible row - } - - // Button to add a new profile - newBtn := widget.NewButton("New Profile", func() { - nameEntry := widget.NewEntry() - nameEntry.SetPlaceHolder("Enter Profile Name") - - formItems := []*widget.FormItem{{Text: "Name:", Widget: nameEntry}} - dlg := dialog.NewForm( - "New Profile", - "Create", - "Cancel", - formItems, - func(confirm bool) { - if !confirm { - return - } - name := nameEntry.Text - if name == "" { - dialog.ShowError(errors.New("profile name cannot be empty"), s.wProfiles) - return - } - - // add profile - err = s.addProfile(name) - if err != nil { - log.Errorf("failed to create profile: %v", err) - dialog.ShowError(fmt.Errorf("failed to create profile"), s.wProfiles) - return - } - dialog.ShowInformation( - "Profile Created", - fmt.Sprintf("Profile '%s' created successfully", name), - s.wProfiles, - ) - // update slice - refresh() - }, - s.wProfiles, - ) - // make dialog wider - dlg.Resize(fyne.NewSize(350, 150)) - dlg.Show() - }) - - // Assemble window content - content := container.NewBorder(nil, newBtn, nil, nil, list) - s.wProfiles = s.app.NewWindow("NetBird Profiles") - s.wProfiles.SetContent(content) - s.wProfiles.Resize(fyne.NewSize(400, 300)) - s.wProfiles.SetOnClosed(s.cancel) - - s.wProfiles.Show() -} - -func (s *serviceClient) addProfile(profileName string) error { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return fmt.Errorf(getClientFMT, err) - } - - currUser, err := user.Current() - if err != nil { - return fmt.Errorf("get current user: %w", err) - } - - _, err = conn.AddProfile(s.ctx, &proto.AddProfileRequest{ - ProfileName: profileName, - Username: currUser.Username, - }) - - if err != nil { - return fmt.Errorf("add profile: %w", err) - } - - return nil -} - -func (s *serviceClient) switchProfile(handle string) error { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return fmt.Errorf(getClientFMT, err) - } - - currUser, err := user.Current() - if err != nil { - return fmt.Errorf("get current user: %w", err) - } - - resp, err := conn.SwitchProfile(s.ctx, &proto.SwitchProfileRequest{ - ProfileName: &handle, - Username: &currUser.Username, - }) - if err != nil { - return fmt.Errorf("switch profile failed: %w", err) - } - - if err := s.profileManager.SwitchProfile(profilemanager.ID(resp.Id)); err != nil { - return fmt.Errorf("switch profile: %w", err) - } - - return nil -} - -func (s *serviceClient) removeProfile(profileName string) error { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return fmt.Errorf(getClientFMT, err) - } - - currUser, err := user.Current() - if err != nil { - return fmt.Errorf("get current user: %w", err) - } - - _, err = conn.RemoveProfile(s.ctx, &proto.RemoveProfileRequest{ - ProfileName: profileName, - Username: currUser.Username, - }) - if err != nil { - return fmt.Errorf("remove profile: %w", err) - } - - return nil -} - -type Profile struct { - ID string - Name string - IsActive bool -} - -// formatProfileLabel returns the display label for a profile. Profiles can -// share the same Name, so when more than one profile in profiles carries this -// Name, a short form of the ID is appended to disambiguate the entries. -func formatProfileLabel(profile Profile, profiles []Profile) string { - count := 0 - for _, p := range profiles { - if p.Name == profile.Name { - count++ - } - } - if count <= 1 { - return profile.Name - } - return fmt.Sprintf("%s (%s)", profile.Name, profilemanager.ID(profile.ID).ShortID()) -} - -func (s *serviceClient) getProfiles() ([]Profile, error) { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - return nil, fmt.Errorf(getClientFMT, err) - } - - currUser, err := user.Current() - if err != nil { - return nil, fmt.Errorf("get current user: %w", err) - } - profilesResp, err := conn.ListProfiles(s.ctx, &proto.ListProfilesRequest{ - Username: currUser.Username, - }) - if err != nil { - return nil, fmt.Errorf("list profiles: %w", err) - } - - var profiles []Profile - - for _, profile := range profilesResp.Profiles { - profiles = append(profiles, Profile{ - ID: profile.Id, - Name: profile.Name, - IsActive: profile.IsActive, - }) - } - - return profiles, nil -} - -func (s *serviceClient) handleProfileLogout(profile Profile, refreshCallback func()) { - dialog.ShowConfirm( - "Deregister", - fmt.Sprintf("Are you sure you want to deregister from '%s'?", profile.Name), - func(confirm bool) { - if !confirm { - return - } - - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - log.Errorf("failed to get service client: %v", err) - dialog.ShowError(fmt.Errorf("failed to connect to service"), s.wProfiles) - return - } - - currUser, err := user.Current() - if err != nil { - log.Errorf("failed to get current user: %v", err) - dialog.ShowError(fmt.Errorf("failed to get current user"), s.wProfiles) - return - } - - username := currUser.Username - // ProfileName is treated as a handle; send the ID so the - // daemon resolves to exactly this profile. - _, err = conn.Logout(s.ctx, &proto.LogoutRequest{ - ProfileName: &profile.ID, - Username: &username, - }) - if err != nil { - log.Errorf("logout failed: %v", err) - dialog.ShowError(fmt.Errorf("deregister failed"), s.wProfiles) - return - } - - dialog.ShowInformation( - "Deregistered", - fmt.Sprintf("Successfully deregistered from '%s'", profile.Name), - s.wProfiles, - ) - - refreshCallback() - }, - s.wProfiles, - ) -} - -type subItem struct { - *systray.MenuItem - ctx context.Context - cancel context.CancelFunc -} - -type profileMenu struct { - mu sync.Mutex - ctx context.Context - serviceClient *serviceClient - profileManager *profilemanager.ProfileManager - eventHandler *eventHandler - profileMenuItem *systray.MenuItem - emailMenuItem *systray.MenuItem - profileSubItems []*subItem - manageProfilesSubItem *subItem - logoutSubItem *subItem - profilesState []Profile - downClickCallback func() error - upClickCallback func(context.Context) error - getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) - loadSettingsCallback func() - app fyne.App -} - -type newProfileMenuArgs struct { - ctx context.Context - serviceClient *serviceClient - profileManager *profilemanager.ProfileManager - eventHandler *eventHandler - profileMenuItem *systray.MenuItem - emailMenuItem *systray.MenuItem - downClickCallback func() error - upClickCallback func(context.Context) error - getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) - loadSettingsCallback func() - app fyne.App -} - -func newProfileMenu(args newProfileMenuArgs) *profileMenu { - p := profileMenu{ - ctx: args.ctx, - serviceClient: args.serviceClient, - profileManager: args.profileManager, - eventHandler: args.eventHandler, - profileMenuItem: args.profileMenuItem, - emailMenuItem: args.emailMenuItem, - downClickCallback: args.downClickCallback, - upClickCallback: args.upClickCallback, - getSrvClientCallback: args.getSrvClientCallback, - loadSettingsCallback: args.loadSettingsCallback, - app: args.app, - } - - p.emailMenuItem.Disable() - p.emailMenuItem.Hide() - p.refresh() - go p.updateMenu() - - return &p -} - -func (p *profileMenu) getProfiles() ([]Profile, error) { - conn, err := p.getSrvClientCallback(defaultFailTimeout) - if err != nil { - return nil, fmt.Errorf(getClientFMT, err) - } - currUser, err := user.Current() - if err != nil { - return nil, fmt.Errorf("get current user: %w", err) - } - - profilesResp, err := conn.ListProfiles(p.ctx, &proto.ListProfilesRequest{ - Username: currUser.Username, - }) - if err != nil { - return nil, fmt.Errorf("list profiles: %w", err) - } - - var profiles []Profile - - for _, profile := range profilesResp.Profiles { - profiles = append(profiles, Profile{ - ID: profile.Id, - Name: profile.Name, - IsActive: profile.IsActive, - }) - } - - return profiles, nil -} - -func (p *profileMenu) refresh() { - p.mu.Lock() - defer p.mu.Unlock() - - profiles, err := p.getProfiles() - if err != nil { - log.Errorf("failed to list profiles: %v", err) - return - } - - // Clear existing profile items - p.clear(profiles) - - currUser, err := user.Current() - if err != nil { - log.Errorf("failed to get current user: %v", err) - return - } - - conn, err := p.getSrvClientCallback(defaultFailTimeout) - if err != nil { - log.Errorf("failed to get daemon client: %v", err) - return - } - - activeProf, err := conn.GetActiveProfile(p.ctx, &proto.GetActiveProfileRequest{}) - if err != nil { - log.Errorf("failed to get active profile: %v", err) - return - } - - if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username { - activeProfState, err := p.profileManager.GetProfileState(profilemanager.ID(activeProf.Id)) - if err != nil { - log.Warnf("failed to get active profile state: %v", err) - p.emailMenuItem.Hide() - } else if activeProfState.Email != "" { - p.emailMenuItem.SetTitle(fmt.Sprintf("(%s)", activeProfState.Email)) - p.emailMenuItem.Show() - } - } - - for _, profile := range profiles { - item := p.profileMenuItem.AddSubMenuItem(formatProfileLabel(profile, profiles), "") - if profile.IsActive { - item.Check() - } - - ctx, cancel := context.WithCancel(context.Background()) - p.profileSubItems = append(p.profileSubItems, &subItem{item, ctx, cancel}) - - go func() { - for { - select { - case <-ctx.Done(): - return // context cancelled - case _, ok := <-item.ClickedCh: - if !ok { - return // channel closed - } - - // Handle profile selection - if profile.IsActive { - log.Infof("Profile '%s' is already active", profile.Name) - return - } - conn, err := p.getSrvClientCallback(defaultFailTimeout) - if err != nil { - log.Errorf("failed to get daemon client: %v", err) - return - } - - switchResp, err := conn.SwitchProfile(ctx, &proto.SwitchProfileRequest{ - ProfileName: &profile.ID, - Username: &currUser.Username, - }) - if err != nil { - log.Errorf("failed to switch profile: %v", err) - // show notification dialog - p.serviceClient.notifier.Send("Error", "Failed to switch profile") - return - } - - err = p.profileManager.SwitchProfile(profilemanager.ID(switchResp.Id)) - if err != nil { - log.Errorf("failed to switch profile '%s': %v", profile.Name, err) - return - } - - log.Infof("Switched to profile '%s'", profile.Name) - - status, err := conn.Status(ctx, &proto.StatusRequest{}) - if err != nil { - log.Errorf("failed to get status after switching profile: %v", err) - return - } - - if status.Status == string(internal.StatusConnected) { - if err := p.downClickCallback(); err != nil { - log.Errorf("failed to handle down click after switching profile: %v", err) - } - } - - if p.serviceClient.connectCancel != nil { - p.serviceClient.connectCancel() - } - - connectCtx, connectCancel := context.WithCancel(p.ctx) - p.serviceClient.connectCancel = connectCancel - - if err := p.upClickCallback(connectCtx); err != nil { - log.Errorf("failed to handle up click after switching profile: %v", err) - } - - connectCancel() - - p.refresh() - p.loadSettingsCallback() - } - } - }() - - } - ctx, cancel := context.WithCancel(context.Background()) - manageItem := p.profileMenuItem.AddSubMenuItem("Manage Profiles", "") - p.manageProfilesSubItem = &subItem{manageItem, ctx, cancel} - - go func() { - for { - select { - case <-ctx.Done(): - return - case _, ok := <-manageItem.ClickedCh: - if !ok { - return - } - p.eventHandler.runSelfCommand(p.ctx, "profiles", "true") - p.refresh() - p.loadSettingsCallback() - } - } - }() - - // Add Logout menu item - ctx2, cancel2 := context.WithCancel(context.Background()) - logoutItem := p.profileMenuItem.AddSubMenuItem("Deregister", "") - p.logoutSubItem = &subItem{logoutItem, ctx2, cancel2} - - go func() { - for { - select { - case <-ctx2.Done(): - return - case _, ok := <-logoutItem.ClickedCh: - if !ok { - return - } - if err := p.eventHandler.logout(p.ctx); err != nil { - log.Errorf("logout failed: %v", err) - p.serviceClient.notifier.Send("Error", "Failed to deregister") - } else { - p.serviceClient.notifier.Send("Success", "Deregistered successfully") - } - } - } - }() - - if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username { - p.profileMenuItem.SetTitle(activeProf.ProfileName) - } else { - p.profileMenuItem.SetTitle(fmt.Sprintf("Profile: %s (User: %s)", activeProf.ProfileName, activeProf.Username)) - p.emailMenuItem.Hide() - } - -} - -func (p *profileMenu) clear(profiles []Profile) { - for _, item := range p.profileSubItems { - item.Remove() - item.cancel() - } - p.profileSubItems = make([]*subItem, 0, len(profiles)) - p.profilesState = profiles - - if p.manageProfilesSubItem != nil { - p.manageProfilesSubItem.Remove() - p.manageProfilesSubItem.cancel() - p.manageProfilesSubItem = nil - } - - if p.logoutSubItem != nil { - p.logoutSubItem.Remove() - p.logoutSubItem.cancel() - p.logoutSubItem = nil - } -} - -// setEnabled greys out (Disable) the profile menu and every existing -// sub-item when the daemon reports the kill switch active, so the user -// sees the menu but cannot enter "Manage Profiles" or switch profile. -// Previously this used Hide() on the parent, but Fyne's systray on -// Windows does not propagate Hide() to a parent that already has -// children — the submenu kept popping up and accepting clicks. Disable -// is the reliable visual lock. -func (p *profileMenu) setEnabled(enabled bool) { - if p.profileMenuItem == nil { - return - } - p.mu.Lock() - defer p.mu.Unlock() - - if enabled { - p.profileMenuItem.Enable() - p.profileMenuItem.SetTooltip("") - } else { - p.profileMenuItem.Disable() - p.profileMenuItem.SetTooltip("Profiles are disabled by daemon") - } - - apply := func(item *systray.MenuItem) { - if item == nil { - return - } - if enabled { - item.Enable() - } else { - item.Disable() - } - } - for _, sub := range p.profileSubItems { - if sub != nil { - apply(sub.MenuItem) - } - } - if p.manageProfilesSubItem != nil { - apply(p.manageProfilesSubItem.MenuItem) - } - if p.logoutSubItem != nil { - apply(p.logoutSubItem.MenuItem) - } -} - -func (p *profileMenu) updateMenu() { - // check every second - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - // get profilesList - profiles, err := p.getProfiles() - if err != nil { - log.Errorf("failed to list profiles: %v", err) - continue - } - - sort.Slice(profiles, func(i, j int) bool { - if profiles[i].Name != profiles[j].Name { - return profiles[i].Name < profiles[j].Name - } - return profiles[i].ID < profiles[j].ID - }) - - p.mu.Lock() - state := p.profilesState - p.mu.Unlock() - - sort.Slice(state, func(i, j int) bool { - return state[i].Name < state[j].Name - }) - - if slices.Equal(profiles, state) { - continue - } - - p.refresh() - case <-p.ctx.Done(): - return // context cancelled - - } - } -} diff --git a/client/ui/quickactions.go b/client/ui/quickactions.go deleted file mode 100644 index bf47ac434..000000000 --- a/client/ui/quickactions.go +++ /dev/null @@ -1,349 +0,0 @@ -//go:build !(linux && 386) - -//go:generate fyne bundle -o quickactions_assets.go assets/connected.png -//go:generate fyne bundle -o quickactions_assets.go -append assets/disconnected.png -package main - -import ( - "context" - _ "embed" - "fmt" - "runtime" - "sync/atomic" - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/widget" - log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/proto" -) - -type quickActionsUiState struct { - connectionStatus string - isToggleButtonEnabled bool - isConnectionChanged bool - toggleAction func() -} - -func newQuickActionsUiState() quickActionsUiState { - return quickActionsUiState{ - connectionStatus: string(internal.StatusIdle), - isToggleButtonEnabled: false, - isConnectionChanged: false, - } -} - -type clientConnectionStatusProvider interface { - connectionStatus(ctx context.Context) (string, error) -} - -type daemonClientConnectionStatusProvider struct { - client proto.DaemonServiceClient -} - -func (d daemonClientConnectionStatusProvider) connectionStatus(ctx context.Context) (string, error) { - childCtx, cancel := context.WithTimeout(ctx, 400*time.Millisecond) - defer cancel() - status, err := d.client.Status(childCtx, &proto.StatusRequest{}) - if err != nil { - return "", err - } - - return status.Status, nil -} - -type clientCommand interface { - execute() error -} - -type connectCommand struct { - connectClient func() error -} - -func (c connectCommand) execute() error { - return c.connectClient() -} - -type disconnectCommand struct { - disconnectClient func() error -} - -func (c disconnectCommand) execute() error { - return c.disconnectClient() -} - -type quickActionsViewModel struct { - provider clientConnectionStatusProvider - connect clientCommand - disconnect clientCommand - uiChan chan quickActionsUiState - isWatchingConnectionStatus atomic.Bool -} - -func newQuickActionsViewModel(ctx context.Context, provider clientConnectionStatusProvider, connect, disconnect clientCommand, uiChan chan quickActionsUiState) { - viewModel := quickActionsViewModel{ - provider: provider, - connect: connect, - disconnect: disconnect, - uiChan: uiChan, - } - - viewModel.isWatchingConnectionStatus.Store(true) - - // base UI status - uiChan <- newQuickActionsUiState() - - // this retrieves the current connection status - // and pushes the UI state that reflects it via uiChan - go viewModel.watchConnectionStatus(ctx) -} - -func (q *quickActionsViewModel) updateUiState(ctx context.Context) { - uiState := newQuickActionsUiState() - connectionStatus, err := q.provider.connectionStatus(ctx) - - if err != nil { - log.Errorf("Status: Error - %v", err) - q.uiChan <- uiState - return - } - - if connectionStatus == string(internal.StatusConnected) { - uiState.toggleAction = func() { - q.executeCommand(q.disconnect) - } - } else { - uiState.toggleAction = func() { - q.executeCommand(q.connect) - } - } - - uiState.isToggleButtonEnabled = true - uiState.connectionStatus = connectionStatus - q.uiChan <- uiState -} - -func (q *quickActionsViewModel) watchConnectionStatus(ctx context.Context) { - ticker := time.NewTicker(1000 * time.Millisecond) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - if q.isWatchingConnectionStatus.Load() { - q.updateUiState(ctx) - } - } - } -} - -func (q *quickActionsViewModel) executeCommand(command clientCommand) { - uiState := newQuickActionsUiState() - // newQuickActionsUiState starts with Idle connection status, - // and all that's necessary here is to just disable the toggle button. - uiState.connectionStatus = "" - - q.uiChan <- uiState - - q.isWatchingConnectionStatus.Store(false) - - err := command.execute() - - if err != nil { - log.Errorf("Status: Error - %v", err) - q.isWatchingConnectionStatus.Store(true) - } else { - uiState = newQuickActionsUiState() - uiState.isConnectionChanged = true - q.uiChan <- uiState - } -} - -func getSystemTrayName() string { - os := runtime.GOOS - switch os { - case "darwin": - return "menu bar" - default: - return "system tray" - } -} - -func (s *serviceClient) getNetBirdImage(name string, content []byte) *canvas.Image { - imageSize := fyne.NewSize(64, 64) - - resource := fyne.NewStaticResource(name, content) - image := canvas.NewImageFromResource(resource) - image.FillMode = canvas.ImageFillContain - image.SetMinSize(imageSize) - image.Resize(imageSize) - - return image -} - -type quickActionsUiComponents struct { - content *fyne.Container - toggleConnectionButton *widget.Button - connectedLabelText, disconnectedLabelText string - connectedImage, disconnectedImage *canvas.Image - connectedCircleRes, disconnectedCircleRes fyne.Resource -} - -// applyQuickActionsUiState applies a single UI state to the quick actions window. -// It closes the window and returns true if the connection status has changed, -// in which case the caller should stop processing further states. -func (s *serviceClient) applyQuickActionsUiState( - uiState quickActionsUiState, - components quickActionsUiComponents, -) bool { - if uiState.isConnectionChanged { - fyne.DoAndWait(func() { - s.wQuickActions.Close() - }) - return true - } - - var logo *canvas.Image - var buttonText string - var buttonIcon fyne.Resource - - if uiState.connectionStatus == string(internal.StatusConnected) { - buttonText = components.connectedLabelText - buttonIcon = components.connectedCircleRes - logo = components.connectedImage - } else if uiState.connectionStatus == string(internal.StatusIdle) { - buttonText = components.disconnectedLabelText - buttonIcon = components.disconnectedCircleRes - logo = components.disconnectedImage - } - - fyne.DoAndWait(func() { - if buttonText != "" { - components.toggleConnectionButton.SetText(buttonText) - } - - if buttonIcon != nil { - components.toggleConnectionButton.SetIcon(buttonIcon) - } - - if uiState.isToggleButtonEnabled { - components.toggleConnectionButton.Enable() - } else { - components.toggleConnectionButton.Disable() - } - - components.toggleConnectionButton.OnTapped = func() { - if uiState.toggleAction != nil { - go uiState.toggleAction() - } - } - - components.toggleConnectionButton.Refresh() - - // the second position in the content's object array is the NetBird logo. - if logo != nil { - components.content.Objects[1] = logo - components.content.Refresh() - } - }) - - return false -} - -// showQuickActionsUI displays a simple window with the NetBird logo and a connection toggle button. -func (s *serviceClient) showQuickActionsUI() { - s.wQuickActions = s.app.NewWindow("NetBird") - vmCtx, vmCancel := context.WithCancel(s.ctx) - s.wQuickActions.SetOnClosed(vmCancel) - - client, err := s.getSrvClient(defaultFailTimeout) - - connCmd := connectCommand{ - connectClient: func() error { - return s.menuUpClick(s.ctx) - }, - } - - disConnCmd := disconnectCommand{ - disconnectClient: func() error { - return s.menuDownClick() - }, - } - - if err != nil { - log.Errorf("get service client: %v", err) - return - } - - uiChan := make(chan quickActionsUiState, 1) - newQuickActionsViewModel(vmCtx, daemonClientConnectionStatusProvider{client: client}, connCmd, disConnCmd, uiChan) - - connectedImage := s.getNetBirdImage("netbird.png", iconAbout) - disconnectedImage := s.getNetBirdImage("netbird-disconnected.png", iconAboutDisconnected) - - connectedCircle := canvas.NewImageFromResource(resourceConnectedPng) - disconnectedCircle := canvas.NewImageFromResource(resourceDisconnectedPng) - - connectedLabelText := "Disconnect" - disconnectedLabelText := "Connect" - - toggleConnectionButton := widget.NewButtonWithIcon(disconnectedLabelText, disconnectedCircle.Resource, func() { - // This button's tap function will be set when an ui state arrives via the uiChan channel. - }) - - // Button starts disabled until the first ui state arrives. - toggleConnectionButton.Disable() - - hintLabelText := fmt.Sprintf("You can always access NetBird from your %s.", getSystemTrayName()) - hintLabel := widget.NewLabel(hintLabelText) - - content := container.NewVBox( - layout.NewSpacer(), - disconnectedImage, - layout.NewSpacer(), - container.NewCenter(toggleConnectionButton), - layout.NewSpacer(), - container.NewCenter(hintLabel), - ) - - // this watches for ui state updates. - go func() { - - for { - select { - case <-vmCtx.Done(): - return - case uiState, ok := <-uiChan: - if !ok { - return - } - - closed := s.applyQuickActionsUiState( - uiState, - quickActionsUiComponents{ - content, - toggleConnectionButton, - connectedLabelText, disconnectedLabelText, - connectedImage, disconnectedImage, - connectedCircle.Resource, disconnectedCircle.Resource, - }, - ) - if closed { - return - } - } - } - }() - - s.wQuickActions.SetContent(content) - s.wQuickActions.Resize(fyne.NewSize(400, 200)) - s.wQuickActions.SetFixedSize(true) - s.wQuickActions.Show() -} diff --git a/client/ui/quickactions_assets.go b/client/ui/quickactions_assets.go deleted file mode 100644 index 9ff5e85a2..000000000 --- a/client/ui/quickactions_assets.go +++ /dev/null @@ -1,23 +0,0 @@ -// auto-generated -// Code generated by '$ fyne bundle'. DO NOT EDIT. - -package main - -import ( - _ "embed" - "fyne.io/fyne/v2" -) - -//go:embed assets/connected.png -var resourceConnectedPngData []byte -var resourceConnectedPng = &fyne.StaticResource{ - StaticName: "assets/connected.png", - StaticContent: resourceConnectedPngData, -} - -//go:embed assets/disconnected.png -var resourceDisconnectedPngData []byte -var resourceDisconnectedPng = &fyne.StaticResource{ - StaticName: "assets/disconnected.png", - StaticContent: resourceDisconnectedPngData, -} diff --git a/client/ui/recenter_linux.go b/client/ui/recenter_linux.go new file mode 100644 index 000000000..25c468d7f --- /dev/null +++ b/client/ui/recenter_linux.go @@ -0,0 +1,13 @@ +//go:build linux && !(linux && 386) + +package main + +// recenterOnShowPredicate returns a per-show predicate; re-centering is only +// needed under the minimal-WM / in-process-XEmbed-tray environment, which neither +// centers small windows nor restores position across hide -> show. Evaluated per +// show, not at startup, because the XEmbed tray can appear after the UI starts +// (panel and autostarted app race at login); xembedTrayAvailable is a cheap, +// side-effect-free probe safe to call repeatedly. +func recenterOnShowPredicate() func() bool { + return xembedTrayAvailable +} diff --git a/client/ui/recenter_other.go b/client/ui/recenter_other.go new file mode 100644 index 000000000..18ffe8730 --- /dev/null +++ b/client/ui/recenter_other.go @@ -0,0 +1,10 @@ +//go:build (!linux || (linux && 386)) && !freebsd && !android && !ios && !js + +package main + +// recenterOnShowPredicate returns nil off Linux: macOS and Windows WMs restore +// window position across hide -> show themselves, so Go-side re-centering would +// only fight a window the user moved. +func recenterOnShowPredicate() func() bool { + return nil +} diff --git a/client/ui/services/autostart.go b/client/ui/services/autostart.go new file mode 100644 index 000000000..f7e3aeea0 --- /dev/null +++ b/client/ui/services/autostart.go @@ -0,0 +1,52 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + "errors" + "fmt" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +// Autostart facade over Wails' AutostartManager. The OS login-item registration +// is the single source of truth; nothing is mirrored to preferences. +type Autostart struct { + mgr *application.AutostartManager +} + +func NewAutostart(mgr *application.AutostartManager) *Autostart { + return &Autostart{mgr: mgr} +} + +func (a *Autostart) Supported(_ context.Context) bool { + _, err := a.mgr.Status() + return !errors.Is(err, application.ErrAutostartNotSupported) +} + +// IsEnabled returns false without error on unsupported platforms. +func (a *Autostart) IsEnabled(_ context.Context) (bool, error) { + enabled, err := a.mgr.IsEnabled() + if err != nil { + if errors.Is(err, application.ErrAutostartNotSupported) { + return false, nil + } + return false, fmt.Errorf("read autostart state: %w", err) + } + return enabled, nil +} + +// SetEnabled takes effect on the next login, not immediately. +func (a *Autostart) SetEnabled(_ context.Context, enabled bool) error { + if enabled { + if err := a.mgr.Enable(); err != nil { + return fmt.Errorf("enable autostart: %w", err) + } + return nil + } + if err := a.mgr.Disable(); err != nil { + return fmt.Errorf("disable autostart: %w", err) + } + return nil +} diff --git a/client/ui/services/compat.go b/client/ui/services/compat.go new file mode 100644 index 000000000..0cc6dd99f --- /dev/null +++ b/client/ui/services/compat.go @@ -0,0 +1,40 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/proto" +) + +// Compat answers whether the running daemon is new enough to drive this UI. +type Compat struct { + conn DaemonConn +} + +func NewCompat(conn DaemonConn) *Compat { + return &Compat{conn: conn} +} + +// DaemonReady probes the WailsUIReady RPC once. A true result means the daemon +// implements it and is compatible. An Unimplemented response means the daemon +// predates this UI and is too old; the caller should surface an upgrade prompt. +// Any other error (daemon not running, transport failure) is returned so the +// frontend can tell "outdated" apart from "not reachable". +func (c *Compat) DaemonReady(ctx context.Context) (bool, error) { + client, err := c.conn.Client() + if err != nil { + return false, err + } + if _, err := client.WailsUIReady(ctx, &proto.WailsUIReadyRequest{}); err != nil { + if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/client/ui/services/conn.go b/client/ui/services/conn.go new file mode 100644 index 000000000..531abe7d9 --- /dev/null +++ b/client/ui/services/conn.go @@ -0,0 +1,13 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import "github.com/netbirdio/netbird/client/proto" + +// DaemonConn returns a lazy gRPC client to the NetBird daemon. +// All services receive a DaemonConn so they share a single connection. +type DaemonConn interface { + Client() (proto.DaemonServiceClient, error) +} + +func ptrStr(s string) *string { return &s } diff --git a/client/ui/services/connection.go b/client/ui/services/connection.go new file mode 100644 index 000000000..a23a526e6 --- /dev/null +++ b/client/ui/services/connection.go @@ -0,0 +1,223 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + "fmt" + "os" + "os/exec" + "os/user" + "runtime" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/proto" +) + +// LoginParams are the inputs to Login. +type LoginParams struct { + ProfileName string `json:"profileName"` + Username string `json:"username"` + ManagementURL string `json:"managementUrl"` + SetupKey string `json:"setupKey"` + PreSharedKey string `json:"preSharedKey"` + Hostname string `json:"hostname"` + Hint string `json:"hint"` +} + +// LoginResult is the daemon's reply to Login. +type LoginResult struct { + NeedsSSOLogin bool `json:"needsSsoLogin"` + UserCode string `json:"userCode"` + VerificationURI string `json:"verificationUri"` + VerificationURIComplete string `json:"verificationUriComplete"` +} + +// WaitSSOParams are the inputs to WaitSSOLogin. +type WaitSSOParams struct { + UserCode string `json:"userCode"` + Hostname string `json:"hostname"` +} + +// UpParams selects the profile to bring up. +type UpParams struct { + ProfileName string `json:"profileName"` + Username string `json:"username"` +} + +// LogoutParams selects the profile to log out. +type LogoutParams struct { + ProfileName string `json:"profileName"` + Username string `json:"username"` +} + +// Connection groups the daemon RPCs that drive login / connect / disconnect. +type Connection struct { + conn DaemonConn + classifier errorClassifier +} + +// NewConnection wires up a Connection. translator or prefs may be nil, in which +// case classifyDaemonError falls back to the bare error key. +func NewConnection(conn DaemonConn, translator ErrorTranslator, prefs LanguagePreference) *Connection { + return &Connection{conn: conn, classifier: errorClassifier{translator: translator, prefs: prefs}} +} + +func (s *Connection) Login(ctx context.Context, p LoginParams) (LoginResult, error) { + cli, err := s.conn.Client() + if err != nil { + return LoginResult{}, err + } + + // No pre-Login Down: Login dislodges a pending WaitSSOLogin itself, and a + // defensive Down would only flash an Idle blink in the tray during handoff. + + // Fall back to the daemon's active profile and the current OS user. + profileName := p.ProfileName + username := p.Username + if profileName == "" { + if active, aerr := cli.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{}); aerr == nil { + // Address the active profile by ID (the daemon resolves it as a + // handle); names can collide, the ID cannot. + profileName = active.GetId() + if username == "" { + username = active.GetUsername() + } + } + } + if username == "" { + if u, uerr := user.Current(); uerr == nil { + username = u.Username + } + } + + req := &proto.LoginRequest{ + ManagementUrl: p.ManagementURL, + SetupKey: p.SetupKey, + Hostname: p.Hostname, + IsUnixDesktopClient: runtime.GOOS == "linux", + } + if profileName != "" { + req.ProfileName = ptrStr(profileName) + } + if username != "" { + req.Username = ptrStr(username) + } + if p.PreSharedKey != "" { + req.OptionalPreSharedKey = ptrStr(p.PreSharedKey) + } + if p.Hint != "" { + req.Hint = ptrStr(p.Hint) + } + + resp, err := cli.Login(ctx, req) + if err != nil { + return LoginResult{}, s.classifyDaemonError(err) + } + return LoginResult{ + NeedsSSOLogin: resp.GetNeedsSSOLogin(), + UserCode: resp.GetUserCode(), + VerificationURI: resp.GetVerificationURI(), + VerificationURIComplete: resp.GetVerificationURIComplete(), + }, nil +} + +func (s *Connection) WaitSSOLogin(ctx context.Context, p WaitSSOParams) (string, error) { + cli, err := s.conn.Client() + if err != nil { + return "", err + } + resp, err := cli.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{ + UserCode: p.UserCode, + Hostname: p.Hostname, + }) + if err != nil { + return "", s.classifyDaemonError(err) + } + return resp.GetEmail(), nil +} + +func (s *Connection) Up(ctx context.Context, p UpParams) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + // Always async: status updates flow via SubscribeStatus. + req := &proto.UpRequest{Async: true} + if p.ProfileName != "" { + req.ProfileName = ptrStr(p.ProfileName) + } + if p.Username != "" { + req.Username = ptrStr(p.Username) + } + if _, err = cli.Up(ctx, req); err != nil { + return s.classifyDaemonError(err) + } + return nil +} + +func (s *Connection) Down(ctx context.Context) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + if _, err = cli.Down(ctx, &proto.DownRequest{}); err != nil { + return s.classifyDaemonError(err) + } + return nil +} + +// OpenURL opens url in an external browser; the embedded webview blocks +// window.open, so the SSO verification page can't pop inline. Honors $BROWSER +// before the platform default. +func (s *Connection) OpenURL(url string) error { + if browser := os.Getenv("BROWSER"); browser != "" { + return exec.Command(browser, url).Start() + } + switch runtime.GOOS { + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + return exec.Command("open", url).Start() + case "linux": + return exec.Command("xdg-open", url).Start() + default: + return fmt.Errorf("unsupported platform") + } +} + +func (s *Connection) Logout(ctx context.Context, p LogoutParams) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + req := &proto.LogoutRequest{} + if p.ProfileName != "" { + req.ProfileName = ptrStr(p.ProfileName) + } + if p.Username != "" { + req.Username = ptrStr(p.Username) + } + if _, err = cli.Logout(ctx, req); err != nil { + return s.classifyDaemonError(err) + } + + // The daemon runs as root and can't reach the user-owned per-profile state + // file holding the account email (see Profiles.List), so clear the stale + // email here; the next SSO login recreates it. + if p.ProfileName != "" { + if err := profilemanager.NewProfileManager().RemoveProfileState(p.ProfileName); err != nil { + // Non-fatal: the logout itself succeeded. + log.Warnf("failed to remove profile state for %s: %v", p.ProfileName, err) + } + } + + return nil +} + +// classifyDaemonError maps a gRPC error to a localised ClientError. +func (s *Connection) classifyDaemonError(err error) *ClientError { + return s.classifier.classify(err) +} diff --git a/client/ui/services/cursor_darwin.go b/client/ui/services/cursor_darwin.go new file mode 100644 index 000000000..a927ff719 --- /dev/null +++ b/client/ui/services/cursor_darwin.go @@ -0,0 +1,42 @@ +//go:build darwin + +package services + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework AppKit +#import +#import + +typedef struct CursorPoint { + int x; + int y; + int ok; +} CursorPoint; + +// NSEvent.mouseLocation is Y-up from primary's bottom-left; flip against +// the primary's frame height so the point matches Wails' Y-down Screen.Bounds. +CursorPoint nbGetCursorPos(void) { + CursorPoint p = {0, 0, 0}; + NSArray *screens = [NSScreen screens]; + if (screens == nil || screens.count == 0) return p; + NSScreen *primary = [screens firstObject]; + if (primary == nil) return p; + NSPoint loc = [NSEvent mouseLocation]; + p.x = (int)loc.x; + p.y = (int)(primary.frame.size.height - loc.y); + p.ok = 1; + return p; +} +*/ +import "C" + +import "github.com/wailsapp/wails/v3/pkg/application" + +func getCursorPosition(_ *application.App) (application.Point, bool) { + res := C.nbGetCursorPos() + if res.ok == 0 { + return application.Point{}, false + } + return application.Point{X: int(res.x), Y: int(res.y)}, true +} diff --git a/client/ui/services/cursor_linux.go b/client/ui/services/cursor_linux.go new file mode 100644 index 000000000..760fd5b86 --- /dev/null +++ b/client/ui/services/cursor_linux.go @@ -0,0 +1,53 @@ +//go:build linux + +package services + +/* +#cgo pkg-config: x11 +#cgo LDFLAGS: -lX11 +#include +#include + +typedef struct CursorPoint { + int x; + int y; + int ok; +} CursorPoint; + +// XQueryPointer works on X11 and, via XWayland, on Wayland sessions. +// ok=0 when no X server is reachable. +CursorPoint nbGetCursorPos(void) { + CursorPoint p = {0, 0, 0}; + Display *dpy = XOpenDisplay(NULL); + if (!dpy) return p; + Window root = DefaultRootWindow(dpy); + if (root == 0) { XCloseDisplay(dpy); return p; } + Window root_return = 0, child_return = 0; + int root_x = 0, root_y = 0, win_x = 0, win_y = 0; + unsigned int mask_return = 0; + if (XQueryPointer(dpy, root, &root_return, &child_return, + &root_x, &root_y, &win_x, &win_y, &mask_return)) { + p.x = root_x; + p.y = root_y; + p.ok = 1; + } + XCloseDisplay(dpy); + return p; +} +*/ +import "C" + +import "github.com/wailsapp/wails/v3/pkg/application" + +func getCursorPosition(app *application.App) (application.Point, bool) { + res := C.nbGetCursorPos() + if res.ok == 0 { + return application.Point{}, false + } + p := application.Point{X: int(res.x), Y: int(res.y)} + // X11 root coords are physical pixels; Screen.Bounds is in DIPs. + if app == nil || app.Screen == nil { + return p, true + } + return app.Screen.PhysicalToDipPoint(p), true +} diff --git a/client/ui/services/cursor_other.go b/client/ui/services/cursor_other.go new file mode 100644 index 000000000..7f35ff438 --- /dev/null +++ b/client/ui/services/cursor_other.go @@ -0,0 +1,9 @@ +//go:build !darwin && !windows && !linux && !freebsd && !android && !ios && !js + +package services + +import "github.com/wailsapp/wails/v3/pkg/application" + +func getCursorPosition(_ *application.App) (application.Point, bool) { + return application.Point{}, false +} diff --git a/client/ui/services/cursor_windows.go b/client/ui/services/cursor_windows.go new file mode 100644 index 000000000..42ed32d74 --- /dev/null +++ b/client/ui/services/cursor_windows.go @@ -0,0 +1,17 @@ +//go:build windows + +package services + +import ( + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/w32" +) + +func getCursorPosition(app *application.App) (application.Point, bool) { + x, y, ok := w32.GetCursorPos() + if !ok || app == nil || app.Screen == nil { + return application.Point{}, false + } + // GetCursorPos is in physical pixels; Screen.Bounds is in DIPs. + return app.Screen.PhysicalToDipPoint(application.Point{X: x, Y: y}), true +} diff --git a/client/ui/services/daemon_feed.go b/client/ui/services/daemon_feed.go new file mode 100644 index 000000000..632581fe9 --- /dev/null +++ b/client/ui/services/daemon_feed.go @@ -0,0 +1,590 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/cenkalti/backoff/v4" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/client/ui/authsession" + "github.com/netbirdio/netbird/client/ui/updater" +) + +const ( + EventStatusSnapshot = "netbird:status" + // EventDaemonNotification carries each SubscribeEvents message. Auto-update + // SystemEvents are also forwarded to updater.Holder.OnSystemEvent so the typed + // update state needs no second daemon subscription. + EventDaemonNotification = "netbird:event" + // EventProfileChanged fires after a daemon-side switch (payload: the new + // ProfileRef). The daemon emits no profile event, so this is the only signal + // that lets a flip driven from one surface paint in the others. + EventProfileChanged = "netbird:profile:changed" + // EventSessionWarning is a typed sibling of EventDaemonNotification so + // subscribers needn't filter the notification firehose. Consumers branch on + // SessionWarning.Final to tell the T-10 event from the T-2 fallback. + EventSessionWarning = "netbird:session:warning" + + // StatusDaemonUnavailable is the synthetic Status emitted when the daemon's + // gRPC socket is unreachable. No internal.Status* collides with this label. + StatusDaemonUnavailable = "DaemonUnavailable" + + // Daemon connection status strings — mirror internal.Status* in + // client/internal/state.go. + StatusConnected = "Connected" + StatusConnecting = "Connecting" + StatusIdle = "Idle" + StatusNeedsLogin = "NeedsLogin" + StatusLoginFailed = "LoginFailed" + StatusSessionExpired = "SessionExpired" + + // SeverityCritical is the lower-cased proto SystemEvent_CRITICAL severity, as + // emitted by systemEventFromProto. Critical events bypass the notifications gate. + SeverityCritical = "critical" +) + +// Emitter sends a named payload to the frontend. Satisfied by Wails app.Event. +type Emitter interface { + Emit(name string, data ...any) bool +} + +// SystemEvent is the frontend-facing shape of a daemon SystemEvent. +type SystemEvent struct { + ID string `json:"id"` + Severity string `json:"severity"` + Category string `json:"category"` + Message string `json:"message"` + UserMessage string `json:"userMessage"` + Timestamp int64 `json:"timestamp"` + Metadata map[string]string `json:"metadata"` +} + +// PeerStatus is the frontend-facing shape of a daemon PeerState. +type PeerStatus struct { + IP string `json:"ip"` + IPv6 string `json:"ipv6"` + PubKey string `json:"pubKey"` + ConnStatus string `json:"connStatus"` + ConnStatusUpdateUnix int64 `json:"connStatusUpdateUnix"` + Relayed bool `json:"relayed"` + LocalIceCandidateType string `json:"localIceCandidateType"` + RemoteIceCandidateType string `json:"remoteIceCandidateType"` + LocalIceCandidateEndpoint string `json:"localIceCandidateEndpoint"` + RemoteIceCandidateEndpoint string `json:"remoteIceCandidateEndpoint"` + Fqdn string `json:"fqdn"` + BytesRx int64 `json:"bytesRx"` + BytesTx int64 `json:"bytesTx"` + LatencyMs int64 `json:"latencyMs"` + RelayAddress string `json:"relayAddress"` + LastHandshakeUnix int64 `json:"lastHandshakeUnix"` + RosenpassEnabled bool `json:"rosenpassEnabled"` + Networks []string `json:"networks"` +} + +// PeerLink is this peer's connection to its mgmt or signal server. +type PeerLink struct { + URL string `json:"url"` + Connected bool `json:"connected"` + Error string `json:"error,omitempty"` +} + +// LocalPeer mirrors LocalPeerState. +type LocalPeer struct { + IP string `json:"ip"` + IPv6 string `json:"ipv6"` + PubKey string `json:"pubKey"` + Fqdn string `json:"fqdn"` + Networks []string `json:"networks"` +} + +// Status is the snapshot the frontend renders on the dashboard. +type Status struct { + Status string `json:"status"` + DaemonVersion string `json:"daemonVersion"` + Management PeerLink `json:"management"` + Signal PeerLink `json:"signal"` + Local LocalPeer `json:"local"` + Peers []PeerStatus `json:"peers"` + Events []SystemEvent `json:"events"` + // NetworksRevision bumps whenever the daemon's routed-networks set or their + // selected state changes, so consumers know when to re-fetch ListNetworks + // instead of polling every snapshot. + NetworksRevision uint64 `json:"networksRevision"` + // SessionExpiresAt is the absolute UTC instant the SSO session expires; nil + // when the peer is not SSO-tracked or login expiration is disabled. + SessionExpiresAt *time.Time `json:"sessionExpiresAt,omitempty"` +} + +// DaemonFeed fans the daemon's two long-running gRPC streams (SubscribeStatus, +// SubscribeEvents) out to the frontend and tray, and exposes a one-shot Status +// RPC for callers wanting the current snapshot without subscribing. +// +// Profile-switch suppression: BeginProfileSwitch makes statusStreamLoop swallow +// the transient stale Connected and Idle pushes the daemon emits during Down, so +// consumers see Connecting → new-profile-state instead of the full blink. +// +// Two flags govern the switch lifecycle, evaluated independently by +// consumeForSwitch on every push because their lifetimes differ: +// +// switchInProgress (suppression): clears on the first real push from the new +// Up. Daemon-side StatusConnecting comes BEFORE any NeedsLogin, so +// suppression must release here before the terminal arrives. +// switchLoginWatch (trigger): outlives suppression. Watches for NeedsLogin +// / LoginFailed / SessionExpired along the Up's retry loop and emits +// EventTriggerLogin so the React orchestrator opens browser-login. +// +// ┌────────────────────────────────────────────┬──────────────────────────────────┐ +// │ Incoming daemon status │ Action │ +// ├────────────────────────────────────────────┼──────────────────────────────────┤ +// │ Connected, Idle (while switchInProgress) │ Suppress (the blink we hide) │ +// │ Connecting │ Emit, clear switchInProgress │ +// │ NeedsLogin, LoginFailed, SessionExpired │ Emit, clear both flags, also │ +// │ │ emit EventTriggerLogin │ +// │ Connected, Idle (while only login-watch) │ Emit, clear switchLoginWatch │ +// │ DaemonUnavailable │ Emit, clear both flags │ +// │ (timeout elapsed) │ Clear flags, emit normally │ +// └────────────────────────────────────────────┴──────────────────────────────────┘ +type DaemonFeed struct { + conn DaemonConn + emitter Emitter + updater *updater.Holder + // logCtl attaches/detaches the GUI file log in response to the daemon's log + // level (a marked SystemEvent on the SubscribeEvents stream). nil when the GUI + // doesn't manage its log (server build / not wired), in which case the marker + // is ignored. + logCtl LogController + + mu sync.Mutex + cancel context.CancelFunc + streamWg sync.WaitGroup + + switchMu sync.Mutex + switchInProgress bool + switchInProgressUntil time.Time + switchLoginWatch bool + switchLoginWatchUntil time.Time +} + +// LogController is the subset of guilog.DebugLog that DaemonFeed drives: Apply +// turns the GUI file log on/off for a daemon level; Path is the gui-client.log +// path to register with the daemon (empty when the GUI doesn't own its log). +type LogController interface { + Apply(level string) + Path() string +} + +// NewDaemonFeed builds the feed. logCtl may be nil (server build / GUI log not +// managed), in which case log-level markers on the event stream are ignored. +func NewDaemonFeed(conn DaemonConn, emitter Emitter, updaterHolder *updater.Holder, logCtl LogController) *DaemonFeed { + return &DaemonFeed{conn: conn, emitter: emitter, updater: updaterHolder, logCtl: logCtl} +} + +// BeginProfileSwitch arms suppression for a switch from Connected/Connecting, +// where the daemon emits stale Connected updates during Down's teardown then an +// Idle before the new Up; statusStreamLoop drops those, and a synthetic +// Connecting snapshot is emitted so consumers paint optimistically. A 30s safety +// timeout clears the flag if no follow-up status arrives. +func (s *DaemonFeed) BeginProfileSwitch() { + now := time.Now() + s.switchMu.Lock() + s.switchInProgress = true + s.switchInProgressUntil = now.Add(30 * time.Second) + s.switchLoginWatch = true + s.switchLoginWatchUntil = now.Add(30 * time.Second) + s.switchMu.Unlock() + s.emitter.Emit(EventStatusSnapshot, Status{Status: StatusConnecting}) +} + +// CancelProfileSwitch aborts a switch midway (tray Disconnect while Connecting): +// clears suppression so the next daemon Idle paints through, and disarms the +// login-watch so the abort doesn't pop a browser-login after the user cancelled. +func (s *DaemonFeed) CancelProfileSwitch() { + s.switchMu.Lock() + s.switchInProgress = false + s.switchLoginWatch = false + s.switchMu.Unlock() +} + +// Watch starts the two background stream loops. Idempotent (a second call while +// running is a no-op); both loops self-restart via exponential backoff. +func (s *DaemonFeed) Watch(ctx context.Context) { + s.mu.Lock() + if s.cancel != nil { + s.mu.Unlock() + return + } + ctx, cancel := context.WithCancel(ctx) + s.cancel = cancel + s.mu.Unlock() + + s.streamWg.Add(2) + go s.statusStreamLoop(ctx) + go s.toastStreamLoop(ctx) +} + +// ServiceShutdown is the Wails service hook fired on app exit. +func (s *DaemonFeed) ServiceShutdown() error { + s.mu.Lock() + cancel := s.cancel + s.cancel = nil + s.mu.Unlock() + if cancel != nil { + cancel() + } + s.streamWg.Wait() + return nil +} + +// Get returns the current daemon status snapshot. An unreachable daemon socket +// yields Status{Status: StatusDaemonUnavailable} rather than an error, so the +// frontend keys off a single status enum without a parallel "error" path. +func (s *DaemonFeed) Get(ctx context.Context) (Status, error) { + cli, err := s.conn.Client() + if err != nil { + if isDaemonUnreachable(err) { + return Status{Status: StatusDaemonUnavailable}, nil + } + return Status{}, err + } + resp, err := cli.Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true}) + if err != nil { + if isDaemonUnreachable(err) { + return Status{Status: StatusDaemonUnavailable}, nil + } + return Status{}, err + } + return statusFromProto(resp), nil +} + +// consumeForSwitch decides, for an incoming push during a profile switch, +// whether to suppress it (suppress) and whether the switch landed in a state +// needing the SSO flow (triggerLogin: NeedsLogin / SessionExpired / LoginFailed). +// +// The two flags have different lifetimes: suppression clears on Connecting, but +// the trigger watcher must survive past it to catch the eventual NeedsLogin — +// daemon-side StatusConnecting fires before loginToManagement, which is what may +// then set StatusNeedsLogin. +func (s *DaemonFeed) consumeForSwitch(st Status) (suppress, triggerLogin bool) { + s.switchMu.Lock() + defer s.switchMu.Unlock() + + now := time.Now() + if s.switchInProgress && now.After(s.switchInProgressUntil) { + s.switchInProgress = false + } + if s.switchLoginWatch && now.After(s.switchLoginWatchUntil) { + s.switchLoginWatch = false + } + + if s.switchInProgress { + switch { + case strings.EqualFold(st.Status, StatusConnecting), + strings.EqualFold(st.Status, StatusNeedsLogin), + strings.EqualFold(st.Status, StatusLoginFailed), + strings.EqualFold(st.Status, StatusSessionExpired), + strings.EqualFold(st.Status, StatusDaemonUnavailable): + // New flow has begun (Up started, or daemon refused it). + s.switchInProgress = false + default: + // Stale Connected from teardown or transient Idle: suppress so the + // optimistic Connecting stays painted. Login-watch stays armed. + return true, false + } + } + + if s.switchLoginWatch { + switch { + case strings.EqualFold(st.Status, StatusNeedsLogin), + strings.EqualFold(st.Status, StatusLoginFailed), + strings.EqualFold(st.Status, StatusSessionExpired): + // SSO-needed terminal: trigger browser-login without a second click. + s.switchLoginWatch = false + return false, true + case strings.EqualFold(st.Status, StatusConnected), + strings.EqualFold(st.Status, StatusIdle), + strings.EqualFold(st.Status, StatusDaemonUnavailable): + // Terminal but not SSO — disarm without triggering. + s.switchLoginWatch = false + } + } + + return false, false +} + +// statusStreamLoop subscribes to SubscribeStatus and re-emits each snapshot on +// the Wails event bus. The first message is the current snapshot; later ones +// fire on connection-state changes only — no polling. +func (s *DaemonFeed) statusStreamLoop(ctx context.Context) { + defer s.streamWg.Done() + + bo := backoff.WithContext(&backoff.ExponentialBackOff{ + InitialInterval: time.Second, + RandomizationFactor: backoff.DefaultRandomizationFactor, + Multiplier: backoff.DefaultMultiplier, + MaxInterval: 10 * time.Second, + MaxElapsedTime: 0, + Stop: backoff.Stop, + Clock: backoff.SystemClock, + }, ctx) + + // unavailable fires the synthetic event once per outage, not on every retry. + unavailable := false + emitUnavailable := func() { + if unavailable { + return + } + unavailable = true + s.emitter.Emit(EventStatusSnapshot, Status{Status: StatusDaemonUnavailable}) + } + + op := func() error { + return s.subscribeAndStreamStatus(ctx, &unavailable, emitUnavailable) + } + + if err := backoff.Retry(op, bo); err != nil && ctx.Err() == nil { + log.Errorf("status stream ended: %v", err) + } +} + +// subscribeAndStreamStatus is one attempt of the status backoff loop: open +// SubscribeStatus and re-emit every snapshot until it errors. A daemon- +// unreachable failure also flips the synthetic-unavailable signal. +func (s *DaemonFeed) subscribeAndStreamStatus(ctx context.Context, unavailable *bool, emitUnavailable func()) error { + cli, err := s.conn.Client() + if err != nil { + emitUnavailable() + return fmt.Errorf("get client: %w", err) + } + stream, err := cli.SubscribeStatus(ctx, &proto.StatusRequest{GetFullPeerStatus: true}) + if err != nil { + if isDaemonUnreachable(err) { + emitUnavailable() + } + return fmt.Errorf("subscribe status: %w", err) + } + for { + resp, err := stream.Recv() + if err != nil { + return s.handleStatusRecvErr(ctx, err, emitUnavailable) + } + *unavailable = false + s.emitStatus(statusFromProto(resp)) + } +} + +// handleStatusRecvErr maps a SubscribeStatus Recv error into the backoff loop's +// return: ctx cancellation stops the loop, an unreachable socket flips the +// synthetic-unavailable signal, everything else is retryable. +func (s *DaemonFeed) handleStatusRecvErr(ctx context.Context, err error, emitUnavailable func()) error { + if ctx.Err() != nil { + return ctx.Err() + } + if isDaemonUnreachable(err) { + emitUnavailable() + } + return fmt.Errorf("status stream recv: %w", err) +} + +// emitStatus pushes a snapshot to the frontend, dropping the transient +// stale-Connected / Idle pushes that occur mid profile switch. +func (s *DaemonFeed) emitStatus(st Status) { + log.Infof("backend event: status status=%q peers=%d", st.Status, len(st.Peers)) + suppress, triggerLogin := s.consumeForSwitch(st) + if suppress { + log.Debugf("suppressing status=%q during profile switch", st.Status) + return + } + s.emitter.Emit(EventStatusSnapshot, st) + if triggerLogin { + s.emitter.Emit(EventTriggerLogin) + } +} + +// toastStreamLoop subscribes to SubscribeEvents and re-emits every SystemEvent +// on the Wails event bus. Local name differs from the RPC so the file's two +// streams aren't both called streamLoop. +func (s *DaemonFeed) toastStreamLoop(ctx context.Context) { + defer s.streamWg.Done() + + bo := backoff.WithContext(&backoff.ExponentialBackOff{ + InitialInterval: time.Second, + RandomizationFactor: backoff.DefaultRandomizationFactor, + Multiplier: backoff.DefaultMultiplier, + MaxInterval: 10 * time.Second, + MaxElapsedTime: 0, + Stop: backoff.Stop, + Clock: backoff.SystemClock, + }, ctx) + + op := func() error { + return s.subscribeAndStreamEvents(ctx) + } + + if err := backoff.Retry(op, bo); err != nil && ctx.Err() == nil { + log.Errorf("event stream ended: %v", err) + } +} + +// subscribeAndStreamEvents is one attempt of the event backoff loop: open +// SubscribeEvents and fan out every SystemEvent until it errors. +func (s *DaemonFeed) subscribeAndStreamEvents(ctx context.Context) error { + cli, err := s.conn.Client() + if err != nil { + return fmt.Errorf("get client: %w", err) + } + stream, err := cli.SubscribeEvents(ctx, &proto.SubscribeRequest{}) + if err != nil { + return fmt.Errorf("subscribe: %w", err) + } + + // Re-register the GUI log path on every (re)connect so a daemon restart + // re-learns it. Best-effort — a failure must not abort the stream. Done even + // when file logging is off, so the path is known ahead of any debug toggle. + if s.logCtl != nil && s.logCtl.Path() != "" { + if _, err := cli.RegisterUILog(ctx, &proto.RegisterUILogRequest{Path: s.logCtl.Path()}); err != nil { + log.Warnf("register UI log path: %v", err) + } + } + for { + ev, err := stream.Recv() + if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + return fmt.Errorf("stream recv: %w", err) + } + s.dispatchSystemEvent(ev) + } +} + +// dispatchSystemEvent fans one daemon SystemEvent out to the frontend +// notification stream, the typed session-warning event (when the metadata +// carries one), and the updater holder (when present). +func (s *DaemonFeed) dispatchSystemEvent(ev *proto.SystemEvent) { + se := systemEventFromProto(ev) + log.Infof("backend event: system severity=%s category=%s msg=%q", se.Severity, se.Category, se.UserMessage) + // Internal refresh signal (CLI-driven profile add/remove), not a notification: + // translate and stop so it never reaches Recent Events or fires an OS toast. + if se.Metadata[proto.MetadataKindKey] == proto.MetadataKindProfileListChanged { + s.emitter.Emit(EventProfileChanged, ProfileRef{}) + return + } + // Internal control signal driving the GUI file log on/off — handle and stop + // so it never reaches Recent Events or toasts. + if se.Metadata[proto.MetadataKindKey] == proto.MetadataKindLogLevelChanged { + if s.logCtl != nil { + s.logCtl.Apply(se.Metadata[proto.MetadataLevelKey]) + } + return + } + s.emitter.Emit(EventDaemonNotification, se) + if warn, ok := authsession.WarningFromMetadata(se.Metadata); ok { + s.emitter.Emit(EventSessionWarning, warn) + } + if s.updater != nil { + s.updater.OnSystemEvent(ev) + } +} + +func statusFromProto(resp *proto.StatusResponse) Status { + full := resp.GetFullStatus() + mgmt := full.GetManagementState() + sig := full.GetSignalState() + local := full.GetLocalPeerState() + + st := Status{ + Status: resp.GetStatus(), + DaemonVersion: resp.GetDaemonVersion(), + NetworksRevision: full.GetNetworksRevision(), + Management: PeerLink{ + URL: mgmt.GetURL(), + Connected: mgmt.GetConnected(), + Error: mgmt.GetError(), + }, + Signal: PeerLink{ + URL: sig.GetURL(), + Connected: sig.GetConnected(), + Error: sig.GetError(), + }, + Local: LocalPeer{ + IP: local.GetIP(), + IPv6: local.GetIpv6(), + PubKey: local.GetPubKey(), + Fqdn: local.GetFqdn(), + Networks: append([]string{}, local.GetNetworks()...), + }, + } + + for _, p := range full.GetPeers() { + st.Peers = append(st.Peers, PeerStatus{ + IP: p.GetIP(), + IPv6: p.GetIpv6(), + PubKey: p.GetPubKey(), + ConnStatus: p.GetConnStatus(), + ConnStatusUpdateUnix: p.GetConnStatusUpdate().GetSeconds(), + Relayed: p.GetRelayed(), + LocalIceCandidateType: p.GetLocalIceCandidateType(), + RemoteIceCandidateType: p.GetRemoteIceCandidateType(), + LocalIceCandidateEndpoint: p.GetLocalIceCandidateEndpoint(), + RemoteIceCandidateEndpoint: p.GetRemoteIceCandidateEndpoint(), + Fqdn: p.GetFqdn(), + BytesRx: p.GetBytesRx(), + BytesTx: p.GetBytesTx(), + LatencyMs: p.GetLatency().AsDuration().Milliseconds(), + RelayAddress: p.GetRelayAddress(), + LastHandshakeUnix: p.GetLastWireguardHandshake().GetSeconds(), + RosenpassEnabled: p.GetRosenpassEnabled(), + Networks: append([]string{}, p.GetNetworks()...), + }) + } + for _, e := range full.GetEvents() { + st.Events = append(st.Events, systemEventFromProto(e)) + } + if ts := resp.GetSessionExpiresAt(); ts.IsValid() && !ts.AsTime().IsZero() { + t := ts.AsTime().UTC() + st.SessionExpiresAt = &t + } + return st +} + +func systemEventFromProto(e *proto.SystemEvent) SystemEvent { + out := SystemEvent{ + ID: e.GetId(), + Severity: strings.ToLower(strings.TrimPrefix(e.GetSeverity().String(), "SystemEvent_")), + Category: strings.ToLower(strings.TrimPrefix(e.GetCategory().String(), "SystemEvent_")), + Message: e.GetMessage(), + UserMessage: e.GetUserMessage(), + Metadata: map[string]string{}, + } + if ts := e.GetTimestamp(); ts != nil { + out.Timestamp = ts.GetSeconds() + } + for k, v := range e.GetMetadata() { + out.Metadata[k] = v + } + return out +} + +// isDaemonUnreachable reports whether a gRPC error means the daemon socket isn't +// answering, versus the daemon responding with an application-level code. Only +// the former should flip the tray to "Not running" — a daemon returning e.g. +// FailedPrecondition is alive and must not be reported as down. +func isDaemonUnreachable(err error) bool { + if err == nil { + return false + } + st, ok := status.FromError(err) + if !ok { + return true + } + return st.Code() == codes.Unavailable +} diff --git a/client/ui/services/debug.go b/client/ui/services/debug.go new file mode 100644 index 000000000..034086747 --- /dev/null +++ b/client/ui/services/debug.go @@ -0,0 +1,136 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + "fmt" + "strings" + "time" + + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/version" +) + +type DebugBundleParams struct { + Anonymize bool `json:"anonymize"` + SystemInfo bool `json:"systemInfo"` + UploadURL string `json:"uploadUrl"` + LogFileCount uint32 `json:"logFileCount"` +} + +// DebugBundleResult: Path is set for local-only bundles, UploadedKey on upload +// success, UploadFailureReason on upload failure. +type DebugBundleResult struct { + Path string `json:"path"` + UploadedKey string `json:"uploadedKey"` + UploadFailureReason string `json:"uploadFailureReason"` +} + +// LogLevel carries a logrus level name: "error", "warn", "info", "debug", "trace". +type LogLevel struct { + Level string `json:"level"` +} + +type Debug struct { + conn DaemonConn +} + +func NewDebug(conn DaemonConn) *Debug { + return &Debug{conn: conn} +} + +func (s *Debug) Bundle(ctx context.Context, p DebugBundleParams) (DebugBundleResult, error) { + cli, err := s.conn.Client() + if err != nil { + return DebugBundleResult{}, err + } + resp, err := cli.DebugBundle(ctx, &proto.DebugBundleRequest{ + Anonymize: p.Anonymize, + SystemInfo: p.SystemInfo, + UploadURL: p.UploadURL, + LogFileCount: p.LogFileCount, + CliVersion: version.NetbirdVersion(), + }) + if err != nil { + return DebugBundleResult{}, err + } + return DebugBundleResult{ + Path: resp.GetPath(), + UploadedKey: resp.GetUploadedKey(), + UploadFailureReason: resp.GetUploadFailureReason(), + }, nil +} + +func (s *Debug) GetLogLevel(ctx context.Context) (LogLevel, error) { + cli, err := s.conn.Client() + if err != nil { + return LogLevel{}, err + } + resp, err := cli.GetLogLevel(ctx, &proto.GetLogLevelRequest{}) + if err != nil { + return LogLevel{}, err + } + return LogLevel{Level: resp.GetLevel().String()}, nil +} + +// RevealFile opens the OS file manager focused on path. Needed because Wails' +// Browser.OpenURL refuses non-http(s) schemes like file://. +func (s *Debug) RevealFile(_ context.Context, path string) error { + if path == "" { + return fmt.Errorf("empty path") + } + return revealFile(path) +} + +// RegisterUILog reports the GUI log path to the daemon for bundle collection; +// the daemon runs as root and can't resolve the user's config dir. Called on +// each daemon (re)connect. +func (s *Debug) RegisterUILog(ctx context.Context, path string) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + _, err = cli.RegisterUILog(ctx, &proto.RegisterUILogRequest{Path: path}) + return err +} + +func (s *Debug) StartBundleCapture(ctx context.Context, timeoutSeconds int32) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + req := &proto.StartBundleCaptureRequest{} + if timeoutSeconds > 0 { + req.Timeout = durationpb.New(time.Duration(timeoutSeconds) * time.Second) + } + _, err = cli.StartBundleCapture(ctx, req) + return err +} + +func (s *Debug) StopBundleCapture(ctx context.Context) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + _, err = cli.StopBundleCapture(ctx, &proto.StopBundleCaptureRequest{}) + return err +} + +func (s *Debug) SetLogLevel(ctx context.Context, lvl LogLevel) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + // proto.LogLevel_value keys are upper-case enum names; callers pass + // lowercase logrus names. Upper-case before lookup or a valid level + // silently falls through to INFO. + level, ok := proto.LogLevel_value[strings.ToUpper(lvl.Level)] + if !ok { + level = int32(proto.LogLevel_INFO) + } + _, err = cli.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel(level)}) + return err +} diff --git a/client/ui/services/debug_reveal_other.go b/client/ui/services/debug_reveal_other.go new file mode 100644 index 000000000..bef0a58bb --- /dev/null +++ b/client/ui/services/debug_reveal_other.go @@ -0,0 +1,20 @@ +//go:build !android && !ios && !freebsd && !js && !windows + +package services + +import ( + "os/exec" + "path/filepath" + "runtime" +) + +// revealFile opens the OS file manager focused on path. +func revealFile(path string) error { + var cmd *exec.Cmd + if runtime.GOOS == "darwin" { + cmd = exec.Command("open", "-R", path) + } else { + cmd = exec.Command("xdg-open", filepath.Dir(path)) + } + return cmd.Start() +} diff --git a/client/ui/services/debug_reveal_windows.go b/client/ui/services/debug_reveal_windows.go new file mode 100644 index 000000000..790d23fab --- /dev/null +++ b/client/ui/services/debug_reveal_windows.go @@ -0,0 +1,54 @@ +package services + +import ( + "fmt" + "os/exec" + "path/filepath" + "unsafe" + + "golang.org/x/sys/windows" +) + +// SW_SHOWNORMAL for ShellExecuteW's nShowCmd. +const swShowNormal = 1 + +var ( + shell32 = windows.NewLazySystemDLL("shell32.dll") + procShellExecute = shell32.NewProc("ShellExecuteW") +) + +// revealFile opens Explorer focused on path. The debug bundle is written by the +// daemon (running as SYSTEM) into C:\Windows\SystemTemp, whose ACL denies the +// logged-in user. A plain "explorer /select" can't traverse it, so we elevate +// via the ShellExecuteW "runas" verb (UAC prompt) — the elevated Explorer can +// read the folder and highlight the file. +func revealFile(path string) error { + verb, err := windows.UTF16PtrFromString("runas") + if err != nil { + return fmt.Errorf("encode verb: %w", err) + } + file, err := windows.UTF16PtrFromString("explorer.exe") + if err != nil { + return fmt.Errorf("encode file: %w", err) + } + params, err := windows.UTF16PtrFromString("/select," + path) + if err != nil { + return fmt.Errorf("encode params: %w", err) + } + + // ShellExecuteW returns an HINSTANCE; a value <=32 is an error code. + ret, _, _ := procShellExecute.Call( + 0, + uintptr(unsafe.Pointer(verb)), + uintptr(unsafe.Pointer(file)), + uintptr(unsafe.Pointer(params)), + 0, + swShowNormal, + ) + if ret <= 32 { + // Elevation declined or failed: fall back to an unelevated reveal of the + // parent directory so the user at least lands near the bundle. + return exec.Command("explorer", filepath.Dir(path)).Start() //nolint:gosec + } + return nil +} diff --git a/client/ui/services/errors.go b/client/ui/services/errors.go new file mode 100644 index 000000000..f1679e764 --- /dev/null +++ b/client/ui/services/errors.go @@ -0,0 +1,133 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "encoding/json" + "strings" + + gcodes "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/ui/i18n" + "github.com/netbirdio/netbird/client/ui/preferences" +) + +// ErrorTranslator localises daemon errors; runtime impl is *i18n.Bundle. +type ErrorTranslator interface { + Translate(lang i18n.LanguageCode, key string, args ...string) string +} + +// LanguagePreference reports the current UI language; runtime impl is *preferences.Store. +type LanguagePreference interface { + Get() preferences.UIPreferences +} + +// ClientError is a structured error returned to the frontend. The frontend +// translates Code via i18n; Short is an English fallback; Long carries the +// unwrapped daemon message. +type ClientError struct { + Code string `json:"code"` + Short string `json:"short"` + Long string `json:"long"` +} + +// Error returns the short message for plain Go callers. +func (e *ClientError) Error() string { + if e == nil { + return "" + } + return e.Short +} + +// MarshalJSON emits the struct so the Wails binding sends an object, not the +// default "error: ..." string. +func (e *ClientError) MarshalJSON() ([]byte, error) { + if e == nil { + return []byte("null"), nil + } + type alias ClientError + return json.Marshal((*alias)(e)) +} + +// errorClassifier maps gRPC errors to a localised ClientError. Shared by the +// daemon-facing services so the frontend gets a clean short message instead of +// the wrapped gRPC chain. +type errorClassifier struct { + translator ErrorTranslator + prefs LanguagePreference +} + +// classify maps a gRPC error to a ClientError by matching known substrings to a +// stable code. A missing locale entry surfaces as a visible "error." +// string — a deliberate fail-loud signal to update the bundle. +func (c errorClassifier) classify(err error) *ClientError { + if err == nil { + return nil + } + + msg := err.Error() + grpcCode := gcodes.Unknown + if st, ok := gstatus.FromError(err); ok { + msg = st.Message() + grpcCode = st.Code() + } + 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" + } + + // Fall back to the gRPC status code when the message didn't match a known + // substring — the daemon now forwards the innermost code with a clean desc + // that no longer contains the English marker text. + if code == "unknown" { + switch grpcCode { + case gcodes.PermissionDenied: + code = "permission_denied" + case gcodes.Unavailable, gcodes.DeadlineExceeded: + code = "daemon_unreachable" + } + } + + return &ClientError{ + Code: code, + Short: c.translateShort(code), + Long: msg, + } +} + +// translateShort resolves the localised short message for code, returning the +// bare "error." key when no translation is available so the gap stays visible. +func (c errorClassifier) translateShort(code string) string { + key := "error." + code + if c.translator == nil { + return key + } + lang := i18n.DefaultLanguage + if c.prefs != nil { + if pref := c.prefs.Get().Language; pref != "" { + lang = pref + } + } + return c.translator.Translate(lang, key) +} diff --git a/client/ui/services/errors_test.go b/client/ui/services/errors_test.go new file mode 100644 index 000000000..2f8f3d039 --- /dev/null +++ b/client/ui/services/errors_test.go @@ -0,0 +1,50 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + gcodes "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" +) + +func TestErrorClassifier_Classify(t *testing.T) { + c := errorClassifier{} // nil translator → Short is the bare "error." key + + t.Run("permission denied by gRPC code with a clean desc", func(t *testing.T) { + // The daemon now forwards the innermost status: code + clean desc that + // no longer carries the English "permission denied" marker. + err := gstatus.Error(gcodes.PermissionDenied, "peer is already registered by a different User or a Setup Key") + + ce := c.classify(err) + require.NotNil(t, ce) + require.Equal(t, "permission_denied", ce.Code) + require.Equal(t, "error.permission_denied", ce.Short) + require.Equal(t, "peer is already registered by a different User or a Setup Key", ce.Long) + }) + + t.Run("substring match still wins for unclassified codes", func(t *testing.T) { + err := gstatus.Error(gcodes.Unknown, "peer login has expired") + + ce := c.classify(err) + require.NotNil(t, ce) + require.Equal(t, "session_expired", ce.Code) + }) + + t.Run("unavailable code maps to daemon_unreachable", func(t *testing.T) { + ce := c.classify(gstatus.Error(gcodes.Unavailable, "transport closing")) + require.Equal(t, "daemon_unreachable", ce.Code) + }) + + t.Run("unmatched stays unknown", func(t *testing.T) { + ce := c.classify(errors.New("something odd")) + require.Equal(t, "unknown", ce.Code) + }) + + t.Run("nil error", func(t *testing.T) { + require.Nil(t, c.classify(nil)) + }) +} diff --git a/client/ui/services/foreground_other.go b/client/ui/services/foreground_other.go new file mode 100644 index 000000000..e6e16a21a --- /dev/null +++ b/client/ui/services/foreground_other.go @@ -0,0 +1,11 @@ +//go:build !windows && !android && !ios && !freebsd && !js + +package services + +import "github.com/wailsapp/wails/v3/pkg/application" + +func raiseToForeground(w *application.WebviewWindow) { + if w != nil { + w.Focus() + } +} diff --git a/client/ui/services/foreground_windows.go b/client/ui/services/foreground_windows.go new file mode 100644 index 000000000..215f30093 --- /dev/null +++ b/client/ui/services/foreground_windows.go @@ -0,0 +1,43 @@ +//go:build windows + +package services + +import ( + "syscall" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/w32" +) + +var procAttachThreadInput = syscall.NewLazyDLL("user32.dll").NewProc("AttachThreadInput") + +func attachThreadInput(attach, attachTo w32.HANDLE, on bool) { + var flag uintptr + if on { + flag = 1 + } + _, _, _ = procAttachThreadInput.Call(uintptr(attach), uintptr(attachTo), flag) +} + +func raiseToForeground(w *application.WebviewWindow) { + if w == nil { + return + } + application.InvokeSync(func() { + ptr := w.NativeWindow() + if ptr == nil { + return + } + hwnd := w32.HWND(uintptr(ptr)) + + fgThread, _ := w32.GetWindowThreadProcessId(w32.GetForegroundWindow()) + appThread := w32.GetCurrentThreadId() + if fgThread != appThread { + attachThreadInput(fgThread, appThread, true) + defer attachThreadInput(fgThread, appThread, false) + } + w32.ShowWindow(hwnd, w32.SW_SHOW) + w32.BringWindowToTop(hwnd) + w32.SetForegroundWindow(hwnd) + }) +} diff --git a/client/ui/services/forwarding.go b/client/ui/services/forwarding.go new file mode 100644 index 000000000..4ba979ad0 --- /dev/null +++ b/client/ui/services/forwarding.go @@ -0,0 +1,83 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + "github.com/netbirdio/netbird/client/proto" +) + +// PortRange is a port range; both ends are inclusive. +type PortRange struct { + Start uint32 `json:"start"` + End uint32 `json:"end"` +} + +// PortInfo holds exactly one of Port or Range (the daemon's oneof). +type PortInfo struct { + Port *uint32 `json:"port,omitempty"` + Range *PortRange `json:"range,omitempty"` +} + +// ForwardingRule is one entry from the daemon's reverse-proxy table. +type ForwardingRule struct { + Protocol string `json:"protocol"` + DestinationPort PortInfo `json:"destinationPort"` + TranslatedAddress string `json:"translatedAddress"` + TranslatedHostname string `json:"translatedHostname"` + TranslatedPort PortInfo `json:"translatedPort"` +} + +// Forwarding groups the daemon RPCs that surface exposed/forwarded services. +type Forwarding struct { + conn DaemonConn +} + +func NewForwarding(conn DaemonConn) *Forwarding { + return &Forwarding{conn: conn} +} + +func (s *Forwarding) List(ctx context.Context) ([]ForwardingRule, error) { + cli, err := s.conn.Client() + if err != nil { + return nil, err + } + resp, err := cli.ForwardingRules(ctx, &proto.EmptyRequest{}) + if err != nil { + return nil, err + } + out := make([]ForwardingRule, 0, len(resp.GetRules())) + for _, r := range resp.GetRules() { + out = append(out, forwardingRuleFromProto(r)) + } + return out, nil +} + +func forwardingRuleFromProto(r *proto.ForwardingRule) ForwardingRule { + return ForwardingRule{ + Protocol: r.GetProtocol(), + DestinationPort: portInfoFromProto(r.GetDestinationPort()), + TranslatedAddress: r.GetTranslatedAddress(), + TranslatedHostname: r.GetTranslatedHostname(), + TranslatedPort: portInfoFromProto(r.GetTranslatedPort()), + } +} + +func portInfoFromProto(p *proto.PortInfo) PortInfo { + if p == nil { + return PortInfo{} + } + switch sel := p.GetPortSelection().(type) { + case *proto.PortInfo_Port: + port := sel.Port + return PortInfo{Port: &port} + case *proto.PortInfo_Range_: + r := sel.Range + if r == nil { + return PortInfo{} + } + return PortInfo{Range: &PortRange{Start: r.GetStart(), End: r.GetEnd()}} + } + return PortInfo{} +} diff --git a/client/ui/services/i18n.go b/client/ui/services/i18n.go new file mode 100644 index 000000000..755bbe02d --- /dev/null +++ b/client/ui/services/i18n.go @@ -0,0 +1,30 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + "github.com/netbirdio/netbird/client/ui/i18n" +) + +// I18n is the Wails-bound facade over i18n.Bundle; the translation logic lives +// in client/ui/i18n. +type I18n struct { + bundle *i18n.Bundle +} + +func NewI18n(bundle *i18n.Bundle) *I18n { + return &I18n{bundle: bundle} +} + +// Languages returns the shipped locales. +func (s *I18n) Languages(_ context.Context) ([]i18n.Language, error) { + return s.bundle.Languages(), nil +} + +// Bundle returns the full key->text map so the React side can drive its own +// translation library off the same source bundles. +func (s *I18n) Bundle(_ context.Context, code i18n.LanguageCode) (map[string]string, error) { + return s.bundle.BundleFor(code) +} diff --git a/client/ui/services/network.go b/client/ui/services/network.go new file mode 100644 index 000000000..c2d127494 --- /dev/null +++ b/client/ui/services/network.go @@ -0,0 +1,88 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + "github.com/netbirdio/netbird/client/proto" +) + +type Network struct { + ID string `json:"id"` + Range string `json:"range"` + Selected bool `json:"selected"` + Domains []string `json:"domains"` + ResolvedIPs map[string][]string `json:"resolvedIps"` +} + +// SelectNetworksParams: All targets every available network; Append merges IDs into the existing selection. +type SelectNetworksParams struct { + NetworkIDs []string `json:"networkIds"` + Append bool `json:"append"` + All bool `json:"all"` +} + +type Networks struct { + conn DaemonConn +} + +func NewNetworks(conn DaemonConn) *Networks { + return &Networks{conn: conn} +} + +func (s *Networks) List(ctx context.Context) ([]Network, error) { + cli, err := s.conn.Client() + if err != nil { + return nil, err + } + resp, err := cli.ListNetworks(ctx, &proto.ListNetworksRequest{}) + if err != nil { + return nil, err + } + out := make([]Network, 0, len(resp.GetRoutes())) + for _, n := range resp.GetRoutes() { + out = append(out, networkFromProto(n)) + } + return out, nil +} + +func (s *Networks) Select(ctx context.Context, p SelectNetworksParams) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + _, err = cli.SelectNetworks(ctx, &proto.SelectNetworksRequest{ + NetworkIDs: p.NetworkIDs, + Append: p.Append, + All: p.All, + }) + return err +} + +func (s *Networks) Deselect(ctx context.Context, p SelectNetworksParams) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + _, err = cli.DeselectNetworks(ctx, &proto.SelectNetworksRequest{ + NetworkIDs: p.NetworkIDs, + Append: p.Append, + All: p.All, + }) + return err +} + +func networkFromProto(n *proto.Network) Network { + resolved := make(map[string][]string, len(n.GetResolvedIPs())) + for k, v := range n.GetResolvedIPs() { + resolved[k] = append([]string{}, v.GetIps()...) + } + return Network{ + ID: n.GetID(), + Range: n.GetRange(), + Selected: n.GetSelected(), + Domains: append([]string{}, n.GetDomains()...), + ResolvedIPs: resolved, + } +} diff --git a/client/ui/services/preferences.go b/client/ui/services/preferences.go new file mode 100644 index 000000000..dae086de8 --- /dev/null +++ b/client/ui/services/preferences.go @@ -0,0 +1,36 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + "github.com/netbirdio/netbird/client/ui/i18n" + "github.com/netbirdio/netbird/client/ui/preferences" +) + +// Preferences is the Wails-bound facade over preferences.Store; the context.Context-first +// signatures are what the binding generator requires. +type Preferences struct { + store *preferences.Store +} + +func NewPreferences(store *preferences.Store) *Preferences { + return &Preferences{store: store} +} + +func (s *Preferences) Get(_ context.Context) (preferences.UIPreferences, error) { + return s.store.Get(), nil +} + +func (s *Preferences) SetLanguage(_ context.Context, lang i18n.LanguageCode) error { + return s.store.SetLanguage(lang) +} + +func (s *Preferences) SetViewMode(_ context.Context, mode preferences.ViewMode) error { + return s.store.SetViewMode(mode) +} + +func (s *Preferences) SetOnboardingCompleted(_ context.Context, done bool) error { + return s.store.SetOnboardingCompleted(done) +} diff --git a/client/ui/services/profile.go b/client/ui/services/profile.go new file mode 100644 index 000000000..09468c9df --- /dev/null +++ b/client/ui/services/profile.go @@ -0,0 +1,179 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + "os/user" + + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/proto" +) + +type Profile struct { + // ID is the daemon-generated on-disk identity of the profile. Display + // names can collide and be renamed, so the ID is the stable handle the + // daemon resolves switch/remove/logout requests against. + ID string `json:"id"` + Name string `json:"name"` + IsActive bool `json:"isActive"` + // Email is read from the user-owned per-profile state file (CLI writes it + // after SSO login), not via ListProfiles: the daemon runs as root and can't + // reach it, while the UI runs as the logged-in user. + Email string `json:"email"` +} + +type ProfileRef struct { + ProfileName string `json:"profileName"` + Username string `json:"username"` +} + +type ActiveProfile struct { + // ID is the active profile's stable on-disk identity. Use it (not the + // display name) as the handle for daemon requests and active-profile + // comparisons, since names can collide. + ID string `json:"id"` + ProfileName string `json:"profileName"` + Username string `json:"username"` +} + +// RenameProfileParams selects a profile by handle and carries its new display +// name. +type RenameProfileParams struct { + // Handle selects the profile to rename: an exact ID, a unique ID prefix, + // or a unique display name. The daemon resolves it server-side. + Handle string `json:"handle"` + // NewName is the new free-form display name. The daemon sanitizes it + // (strips control characters, trims, caps length) but keeps spaces, emoji, + // punctuation, and non-ASCII letters. + NewName string `json:"newName"` + + Username string `json:"username"` +} + +type Profiles struct { + conn DaemonConn +} + +func NewProfiles(conn DaemonConn) *Profiles { + return &Profiles{conn: conn} +} + +// Username returns the OS username the daemon expects for profile lookups. +func (s *Profiles) Username() (string, error) { + u, err := user.Current() + if err != nil { + return "", err + } + return u.Username, nil +} + +func (s *Profiles) List(ctx context.Context, username string) ([]Profile, error) { + cli, err := s.conn.Client() + if err != nil { + return nil, err + } + resp, err := cli.ListProfiles(ctx, &proto.ListProfilesRequest{Username: username}) + if err != nil { + return nil, err + } + pm := profilemanager.NewProfileManager() + out := make([]Profile, 0, len(resp.GetProfiles())) + for _, p := range resp.GetProfiles() { + prof := Profile{ID: p.GetId(), Name: p.GetName(), IsActive: p.GetIsActive()} + if state, err := pm.GetProfileState(profilemanager.ID(p.GetId())); err == nil { + prof.Email = state.Email + } + out = append(out, prof) + } + return out, nil +} + +func (s *Profiles) GetActive(ctx context.Context) (ActiveProfile, error) { + cli, err := s.conn.Client() + if err != nil { + return ActiveProfile{}, err + } + resp, err := cli.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{}) + if err != nil { + return ActiveProfile{}, err + } + return ActiveProfile{ + ID: resp.GetId(), + ProfileName: resp.GetProfileName(), + Username: resp.GetUsername(), + }, nil +} + +// Switch sends a profile switch to the daemon and returns the resolved +// on-disk ID of the now-active profile. ProfileName is treated as a handle +// (exact ID, unique ID prefix, or unique display name); the daemon resolves +// it server-side and echoes back the canonical ID. +func (s *Profiles) Switch(ctx context.Context, p ProfileRef) (string, error) { + cli, err := s.conn.Client() + if err != nil { + return "", err + } + req := &proto.SwitchProfileRequest{} + if p.ProfileName != "" { + req.ProfileName = ptrStr(p.ProfileName) + } + if p.Username != "" { + req.Username = ptrStr(p.Username) + } + resp, err := cli.SwitchProfile(ctx, req) + if err != nil { + return "", err + } + return resp.GetId(), nil +} + +// Add creates a profile with the given display name and returns its +// daemon-generated on-disk ID, so callers can address the new profile by ID +// (e.g. to write config or switch to it) without re-resolving the name. +func (s *Profiles) Add(ctx context.Context, p ProfileRef) (string, error) { + cli, err := s.conn.Client() + if err != nil { + return "", err + } + resp, err := cli.AddProfile(ctx, &proto.AddProfileRequest{ + ProfileName: p.ProfileName, + Username: p.Username, + }) + if err != nil { + return "", err + } + return resp.GetId(), nil +} + +func (s *Profiles) Remove(ctx context.Context, p ProfileRef) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + _, err = cli.RemoveProfile(ctx, &proto.RemoveProfileRequest{ + ProfileName: p.ProfileName, + Username: p.Username, + }) + return err +} + +// Rename changes a profile's display name. The on-disk ID is unaffected, so +// the active profile and any ID-based references stay valid (the default +// profile can be renamed too — only its display name changes). Returns the +// profile's previous display name as confirmation. +func (s *Profiles) Rename(ctx context.Context, p RenameProfileParams) (string, error) { + cli, err := s.conn.Client() + if err != nil { + return "", err + } + resp, err := cli.RenameProfile(ctx, &proto.RenameProfileRequest{ + Username: p.Username, + Handle: p.Handle, + NewProfileName: p.NewName, + }) + if err != nil { + return "", err + } + return resp.GetOldProfileName(), nil +} diff --git a/client/ui/services/profileswitcher.go b/client/ui/services/profileswitcher.go new file mode 100644 index 000000000..c27b62d92 --- /dev/null +++ b/client/ui/services/profileswitcher.go @@ -0,0 +1,92 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + "fmt" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/profilemanager" +) + +// ProfileSwitcher holds the reconnect policy shared by the tray and React +// frontend so both flip profiles identically. The policy keys off prevStatus +// from DaemonFeed.Get at SwitchActive entry: +// +// Connected/Connecting → Switch + Down + Up; optimistic Connecting paint. +// NeedsLogin/LoginFailed/SessionExpired → Switch + Down; clear stale error for re-login. +// Idle → Switch only. +type ProfileSwitcher struct { + profiles *Profiles + connection *Connection + feed *DaemonFeed +} + +func NewProfileSwitcher(profiles *Profiles, connection *Connection, feed *DaemonFeed) *ProfileSwitcher { + return &ProfileSwitcher{profiles: profiles, connection: connection, feed: feed} +} + +// SwitchActive switches to the named profile applying the reconnect policy. +func (s *ProfileSwitcher) SwitchActive(ctx context.Context, p ProfileRef) error { + prevStatus := "" + if st, err := s.feed.Get(ctx); err == nil { + prevStatus = st.Status + } else { + log.Warnf("profileswitcher: get status: %v", err) + } + + wasActive := strings.EqualFold(prevStatus, StatusConnected) || + strings.EqualFold(prevStatus, StatusConnecting) + needsDown := wasActive || + strings.EqualFold(prevStatus, StatusNeedsLogin) || + strings.EqualFold(prevStatus, StatusLoginFailed) || + strings.EqualFold(prevStatus, StatusSessionExpired) + + log.Infof("profileswitcher: switch profile=%q prevStatus=%q wasActive=%v needsDown=%v", + p.ProfileName, prevStatus, wasActive, needsDown) + + // Optimistic Connecting paint only when wasActive: those prevStatuses emit + // stale Connected + transient Idle pushes during Down that must be + // suppressed until Up resumes the stream (see DaemonFeed suppression table). + if wasActive { + s.feed.BeginProfileSwitch() + } + + resolvedID, err := s.profiles.Switch(ctx, p) + if err != nil { + return fmt.Errorf("switch profile %q: %w", p.ProfileName, err) + } + + // Mirror into the user-side ProfileManager state: the CLI's `netbird up` + // reads this file and sends the ID back in the Up RPC, so if it diverges + // the daemon reverts the UI switch on the next CLI `up`. Best-effort — the + // daemon is authoritative; a failure only leaves the CLI's view stale. + // Use the daemon-resolved ID rather than the handle we sent, since the + // on-disk state is keyed by ID, not display name. + if err := profilemanager.NewProfileManager().SwitchProfile(profilemanager.ID(resolvedID)); err != nil { + log.Warnf("profileswitcher: mirror to user-side ProfileManager failed: %v", err) + } + + if needsDown { + if err := s.connection.Down(ctx); err != nil { + log.Errorf("profileswitcher: Down: %v", err) + } + } + + if wasActive { + if err := s.connection.Up(ctx, UpParams(p)); err != nil { + return fmt.Errorf("reconnect %q: %w", p.ProfileName, err) + } + } + + // The daemon emits no profile event, so fan out ourselves or the React + // ProfileContext stays on the old profile after a tray-initiated switch. + if s.feed != nil && s.feed.emitter != nil { + s.feed.emitter.Emit(EventProfileChanged, p) + } + + return nil +} diff --git a/client/ui/services/session.go b/client/ui/services/session.go new file mode 100644 index 000000000..facb8e898 --- /dev/null +++ b/client/ui/services/session.go @@ -0,0 +1,48 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + "github.com/netbirdio/netbird/client/ui/authsession" +) + +// Re-exports so generated bindings reference services.* without importing authsession. +type ( + ExtendStartParams = authsession.ExtendStartParams + ExtendStartResult = authsession.ExtendStartResult + ExtendWaitParams = authsession.ExtendWaitParams + ExtendResult = authsession.ExtendResult +) + +// Session wraps authsession.Session, exposing only the subset the React frontend +// calls; the tray uses authsession.Session directly, keeping the generated TS surface minimal. +type Session struct { + inner *authsession.Session + classifier errorClassifier +} + +// NewSession wraps inner; the caller retains ownership and may use it directly. +// translator or prefs may be nil, in which case errors fall back to the bare code key. +func NewSession(inner *authsession.Session, translator ErrorTranslator, prefs LanguagePreference) *Session { + return &Session{inner: inner, classifier: errorClassifier{translator: translator, prefs: prefs}} +} + +// RequestExtend starts the SSO session-extension flow; the result carries the verification URI to open. +func (s *Session) RequestExtend(ctx context.Context, p ExtendStartParams) (ExtendStartResult, error) { + res, err := s.inner.RequestExtend(ctx, p) + if err != nil { + return ExtendStartResult{}, s.classifier.classify(err) + } + return res, nil +} + +// WaitExtend blocks until the RequestExtend flow completes; the deadline is nil when the peer is ineligible. +func (s *Session) WaitExtend(ctx context.Context, p ExtendWaitParams) (ExtendResult, error) { + res, err := s.inner.WaitExtend(ctx, p) + if err != nil { + return ExtendResult{}, s.classifier.classify(err) + } + return res, nil +} diff --git a/client/ui/services/settings.go b/client/ui/services/settings.go new file mode 100644 index 000000000..44f1191f8 --- /dev/null +++ b/client/ui/services/settings.go @@ -0,0 +1,260 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + "fmt" + "reflect" + + "github.com/netbirdio/netbird/client/proto" +) + +type MDMFields struct { + ManagementURL string `json:"managementURL"` + PreSharedKey bool `json:"preSharedKey"` + WireguardPort bool `json:"wireguardPort"` + RosenpassEnabled bool `json:"rosenpassEnabled"` + RosenpassPermissive bool `json:"rosenpassPermissive"` + DisableClientRoutes bool `json:"disableClientRoutes"` + DisableServerRoutes bool `json:"disableServerRoutes"` + AllowServerSSH *bool `json:"allowServerSSH"` + DisableAutoConnect bool `json:"disableAutoConnect"` + BlockInbound bool `json:"blockInbound"` + DisableMetricsCollection bool `json:"disableMetricsCollection"` + SplitTunnelMode bool `json:"splitTunnelMode"` + SplitTunnelApps bool `json:"splitTunnelApps"` + DisableAdvancedView bool `json:"disableAdvancedView"` +} + +type Features struct { + DisableProfiles bool `json:"disableProfiles"` + DisableNetworks bool `json:"disableNetworks"` + DisableUpdateSettings bool `json:"disableUpdateSettings"` +} + +type Restrictions struct { + MDM MDMFields `json:"mdm"` + Features Features `json:"features"` +} + +type ConfigParams struct { + ProfileName string `json:"profileName"` + Username string `json:"username"` +} + +type Config struct { + ManagementURL string `json:"managementUrl"` + AdminURL string `json:"adminUrl"` + ConfigFile string `json:"configFile"` + LogFile string `json:"logFile"` + PreSharedKeySet bool `json:"preSharedKeySet"` + InterfaceName string `json:"interfaceName"` + WireguardPort int64 `json:"wireguardPort"` + MTU int64 `json:"mtu"` + DisableAutoConnect bool `json:"disableAutoConnect"` + ServerSSHAllowed bool `json:"serverSshAllowed"` + RosenpassEnabled bool `json:"rosenpassEnabled"` + RosenpassPermissive bool `json:"rosenpassPermissive"` + DisableNotifications bool `json:"disableNotifications"` + LazyConnectionEnabled bool `json:"lazyConnectionEnabled"` + BlockInbound bool `json:"blockInbound"` + NetworkMonitor bool `json:"networkMonitor"` + DisableClientRoutes bool `json:"disableClientRoutes"` + DisableServerRoutes bool `json:"disableServerRoutes"` + DisableDNS bool `json:"disableDns"` + DisableIPv6 bool `json:"disableIpv6"` + BlockLANAccess bool `json:"blockLanAccess"` + EnableSSHRoot bool `json:"enableSshRoot"` + EnableSSHSFTP bool `json:"enableSshSftp"` + EnableSSHLocalPortForwarding bool `json:"enableSshLocalPortForwarding"` + EnableSSHRemotePortForwarding bool `json:"enableSshRemotePortForwarding"` + DisableSSHAuth bool `json:"disableSshAuth"` + SSHJWTCacheTTL int32 `json:"sshJwtCacheTtl"` +} + +// SetConfigParams is a partial update — only non-nil pointer fields are sent +// to the daemon; nil fields are preserved. +type SetConfigParams struct { + ProfileName string `json:"profileName"` + Username string `json:"username"` + ManagementURL string `json:"managementUrl"` + AdminURL string `json:"adminUrl"` + InterfaceName *string `json:"interfaceName,omitempty"` + WireguardPort *int64 `json:"wireguardPort,omitempty"` + MTU *int64 `json:"mtu,omitempty"` + PreSharedKey *string `json:"preSharedKey,omitempty"` + DisableAutoConnect *bool `json:"disableAutoConnect,omitempty"` + ServerSSHAllowed *bool `json:"serverSshAllowed,omitempty"` + RosenpassEnabled *bool `json:"rosenpassEnabled,omitempty"` + RosenpassPermissive *bool `json:"rosenpassPermissive,omitempty"` + DisableNotifications *bool `json:"disableNotifications,omitempty"` + LazyConnectionEnabled *bool `json:"lazyConnectionEnabled,omitempty"` + BlockInbound *bool `json:"blockInbound,omitempty"` + NetworkMonitor *bool `json:"networkMonitor,omitempty"` + DisableClientRoutes *bool `json:"disableClientRoutes,omitempty"` + DisableServerRoutes *bool `json:"disableServerRoutes,omitempty"` + DisableDNS *bool `json:"disableDns,omitempty"` + DisableIPv6 *bool `json:"disableIpv6,omitempty"` + DisableFirewall *bool `json:"disableFirewall,omitempty"` + BlockLANAccess *bool `json:"blockLanAccess,omitempty"` + EnableSSHRoot *bool `json:"enableSshRoot,omitempty"` + EnableSSHSFTP *bool `json:"enableSshSftp,omitempty"` + EnableSSHLocalPortForwarding *bool `json:"enableSshLocalPortForwarding,omitempty"` + EnableSSHRemotePortForwarding *bool `json:"enableSshRemotePortForwarding,omitempty"` + DisableSSHAuth *bool `json:"disableSshAuth,omitempty"` + SSHJWTCacheTTL *int32 `json:"sshJwtCacheTtl,omitempty"` +} + +type Settings struct { + conn DaemonConn +} + +func NewSettings(conn DaemonConn) *Settings { + return &Settings{conn: conn} +} + +func (s *Settings) GetConfig(ctx context.Context, p ConfigParams) (Config, error) { + cli, err := s.conn.Client() + if err != nil { + return Config{}, err + } + resp, err := cli.GetConfig(ctx, &proto.GetConfigRequest{ + ProfileName: p.ProfileName, + Username: p.Username, + }) + if err != nil { + return Config{}, err + } + return Config{ + ManagementURL: resp.GetManagementUrl(), + AdminURL: resp.GetAdminURL(), + ConfigFile: resp.GetConfigFile(), + LogFile: resp.GetLogFile(), + PreSharedKeySet: resp.GetPreSharedKey() != "", + InterfaceName: resp.GetInterfaceName(), + WireguardPort: resp.GetWireguardPort(), + MTU: resp.GetMtu(), + DisableAutoConnect: resp.GetDisableAutoConnect(), + ServerSSHAllowed: resp.GetServerSSHAllowed(), + RosenpassEnabled: resp.GetRosenpassEnabled(), + RosenpassPermissive: resp.GetRosenpassPermissive(), + DisableNotifications: resp.GetDisableNotifications(), + LazyConnectionEnabled: resp.GetLazyConnectionEnabled(), + BlockInbound: resp.GetBlockInbound(), + NetworkMonitor: resp.GetNetworkMonitor(), + DisableClientRoutes: resp.GetDisableClientRoutes(), + DisableServerRoutes: resp.GetDisableServerRoutes(), + DisableDNS: resp.GetDisableDns(), + DisableIPv6: resp.GetDisableIpv6(), + BlockLANAccess: resp.GetBlockLanAccess(), + EnableSSHRoot: resp.GetEnableSSHRoot(), + EnableSSHSFTP: resp.GetEnableSSHSFTP(), + EnableSSHLocalPortForwarding: resp.GetEnableSSHLocalPortForwarding(), + EnableSSHRemotePortForwarding: resp.GetEnableSSHRemotePortForwarding(), + DisableSSHAuth: resp.GetDisableSSHAuth(), + SSHJWTCacheTTL: resp.GetSshJWTCacheTTL(), + }, nil +} + +func (s *Settings) SetConfig(ctx context.Context, p SetConfigParams) error { + cli, err := s.conn.Client() + if err != nil { + return err + } + req := &proto.SetConfigRequest{ + ProfileName: p.ProfileName, + Username: p.Username, + ManagementUrl: p.ManagementURL, + AdminURL: p.AdminURL, + InterfaceName: p.InterfaceName, + WireguardPort: p.WireguardPort, + Mtu: p.MTU, + OptionalPreSharedKey: p.PreSharedKey, + DisableAutoConnect: p.DisableAutoConnect, + ServerSSHAllowed: p.ServerSSHAllowed, + RosenpassEnabled: p.RosenpassEnabled, + RosenpassPermissive: p.RosenpassPermissive, + DisableNotifications: p.DisableNotifications, + LazyConnectionEnabled: p.LazyConnectionEnabled, + BlockInbound: p.BlockInbound, + NetworkMonitor: p.NetworkMonitor, + DisableClientRoutes: p.DisableClientRoutes, + DisableServerRoutes: p.DisableServerRoutes, + DisableDns: p.DisableDNS, + DisableIpv6: p.DisableIPv6, + DisableFirewall: p.DisableFirewall, + BlockLanAccess: p.BlockLANAccess, + EnableSSHRoot: p.EnableSSHRoot, + EnableSSHSFTP: p.EnableSSHSFTP, + EnableSSHLocalPortForwarding: p.EnableSSHLocalPortForwarding, + EnableSSHRemotePortForwarding: p.EnableSSHRemotePortForwarding, + DisableSSHAuth: p.DisableSSHAuth, + SshJWTCacheTTL: p.SSHJWTCacheTTL, + } + _, err = cli.SetConfig(ctx, req) + return err +} + +func (s *Settings) GetRestrictions(ctx context.Context) (Restrictions, error) { + cli, err := s.conn.Client() + if err != nil { + return Restrictions{}, err + } + active, err := cli.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{}) + if err != nil { + return Restrictions{}, fmt.Errorf("get active profile: %w", err) + } + cfgResp, err := cli.GetConfig(ctx, &proto.GetConfigRequest{ + ProfileName: active.GetId(), + Username: active.GetUsername(), + }) + if err != nil { + return Restrictions{}, err + } + featResp, err := cli.GetFeatures(ctx, &proto.GetFeaturesRequest{}) + if err != nil { + return Restrictions{}, err + } + r := Restrictions{ + Features: Features{ + DisableProfiles: featResp.GetDisableProfiles(), + DisableNetworks: featResp.GetDisableNetworks(), + DisableUpdateSettings: featResp.GetDisableUpdateSettings(), + }, + } + applyMDMRestrictions(&r.MDM, cfgResp) + r.MDM.DisableAdvancedView = featResp.GetDisableAdvancedView() + return r, nil +} + +func applyMDMRestrictions(mdm *MDMFields, cfgResp *proto.GetConfigResponse) { + managed := cfgResp.GetMDMManagedFields() + if len(managed) == 0 { + return + } + set := make(map[string]struct{}, len(managed)) + for _, k := range managed { + set[k] = struct{}{} + } + v := reflect.ValueOf(mdm).Elem() + t := v.Type() + for i := 0; i < t.NumField(); i++ { + if v.Field(i).Kind() != reflect.Bool { + continue + } + if t.Field(i).Name == "DisableAdvancedView" { + continue + } + if _, ok := set[t.Field(i).Tag.Get("json")]; ok { + v.Field(i).SetBool(true) + } + } + if _, ok := set["managementURL"]; ok { + mdm.ManagementURL = cfgResp.GetManagementUrl() + } + if _, ok := set["allowServerSSH"]; ok { + allowed := cfgResp.GetServerSSHAllowed() + mdm.AllowServerSSH = &allowed + } +} diff --git a/client/ui/services/uilog.go b/client/ui/services/uilog.go new file mode 100644 index 000000000..0d794c137 --- /dev/null +++ b/client/ui/services/uilog.go @@ -0,0 +1,36 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + log "github.com/sirupsen/logrus" +) + +// UILog forwards frontend console output into logrus, tagging the JS origin +// as the "ui" field to stay distinct from logrus's Go-caller source. +type UILog struct{} + +func NewUILog() *UILog { return &UILog{} } + +// Log maps an unrecognised level to info; empty source becomes "unknown". +func (s *UILog) Log(_ context.Context, level, source, msg string) { + origin := "unknown" + if source != "" { + origin = source + } + entry := log.WithField("ui", origin) + switch level { + case "trace": + entry.Trace(msg) + case "debug": + entry.Debug(msg) + case "warn", "warning": + entry.Warn(msg) + case "error": + entry.Error(msg) + default: + entry.Info(msg) + } +} diff --git a/client/ui/services/update.go b/client/ui/services/update.go new file mode 100644 index 000000000..753177d45 --- /dev/null +++ b/client/ui/services/update.go @@ -0,0 +1,73 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/client/ui/updater" +) + +// UpdateResult mirrors TriggerUpdateResponse. +type UpdateResult struct { + Success bool `json:"success"` + ErrorMsg string `json:"errorMsg"` +} + +// Update is the Wails-bound facade over the daemon's update RPCs. The state +// machine and push event live in client/ui/updater. +type Update struct { + conn DaemonConn + holder *updater.Holder +} + +func NewUpdate(conn DaemonConn, holder *updater.Holder) *Update { + return &Update{conn: conn, holder: holder} +} + +func (s *Update) GetState() updater.State { + return s.holder.Get() +} + +// Quit exits the app. Scheduled off the calling goroutine so the JS caller's +// response returns before the runtime tears down. +func (s *Update) Quit() { + go func() { + time.Sleep(100 * time.Millisecond) + application.Get().Quit() + }() +} + +func (s *Update) Trigger(ctx context.Context) (UpdateResult, error) { + cli, err := s.conn.Client() + if err != nil { + return UpdateResult{}, err + } + resp, err := cli.TriggerUpdate(ctx, &proto.TriggerUpdateRequest{}) + if err != nil { + return UpdateResult{}, err + } + return UpdateResult{ + Success: resp.GetSuccess(), + ErrorMsg: resp.GetErrorMsg(), + }, nil +} + +func (s *Update) GetInstallerResult(ctx context.Context) (UpdateResult, error) { + cli, err := s.conn.Client() + if err != nil { + return UpdateResult{}, err + } + resp, err := cli.GetInstallerResult(ctx, &proto.InstallerResultRequest{}) + if err != nil { + return UpdateResult{}, err + } + return UpdateResult{ + Success: resp.GetSuccess(), + ErrorMsg: resp.GetErrorMsg(), + }, nil +} diff --git a/client/ui/services/version.go b/client/ui/services/version.go new file mode 100644 index 000000000..8caea9f72 --- /dev/null +++ b/client/ui/services/version.go @@ -0,0 +1,22 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + "github.com/netbirdio/netbird/version" +) + +// Version reports only the GUI's own version; the daemon version comes from +// the status feed's DaemonVersion field. +type Version struct{} + +func NewVersion() *Version { + return &Version{} +} + +// GUI returns the UI binary's version, stamped via ldflags ("development" if un-stamped). +func (v *Version) GUI(_ context.Context) string { + return version.NetbirdVersion() +} diff --git a/client/ui/services/windowmanager.go b/client/ui/services/windowmanager.go new file mode 100644 index 000000000..3316dadaa --- /dev/null +++ b/client/ui/services/windowmanager.go @@ -0,0 +1,576 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "net/url" + "strconv" + "sync" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" + + "github.com/netbirdio/netbird/client/ui/i18n" + "github.com/netbirdio/netbird/client/ui/preferences" +) + +// LanguageSubscriber delivers UI preference changes so window titles follow the language. +type LanguageSubscriber interface { + Subscribe() (<-chan preferences.UIPreferences, func()) +} + +// EventTriggerLogin asks the frontend's startLogin() to begin an SSO flow. +const EventTriggerLogin = "trigger-login" + +// EventBrowserLoginCancel signals the user dismissed the BrowserLogin popup. +const EventBrowserLoginCancel = "browser-login:cancel" + +// EventSettingsOpen tells the mounted settings window which tab to show. +const EventSettingsOpen = "netbird:settings:open" + +var WindowBackgroundColour = application.NewRGB(24, 26, 29) // bg-nb-gray-950 + +// WindowHeight is shared by the main and Settings windows. +const WindowHeight = 660 + +// Wails reads CustomTheme colours as 0x00BBGGRR (RGB byte order reversed). +var microsoftWindowsTheme = &application.WindowTheme{ + BorderColour: u32ptr(0x00211E1C), + TitleBarColour: u32ptr(0x00211E1C), + TitleTextColour: u32ptr(0x00E9E7E4), +} + +// MicrosoftWindowsAppearanceOptions is the shared Windows chrome (Mica + dark + custom title bar). +func MicrosoftWindowsAppearanceOptions() application.WindowsWindow { + return application.WindowsWindow{ + BackdropType: application.Mica, + Theme: application.Dark, + CustomTheme: application.ThemeSettings{ + DarkModeActive: microsoftWindowsTheme, + DarkModeInactive: microsoftWindowsTheme, + LightModeActive: microsoftWindowsTheme, + LightModeInactive: microsoftWindowsTheme, + }, + } +} + +// AppleMacOSAppearanceOptions is the shared macOS chrome; FullScreenNone keeps the fixed-size layout. +func AppleMacOSAppearanceOptions() application.MacWindow { + return application.MacWindow{ + InvisibleTitleBarHeight: 38, + Backdrop: application.MacBackdropNormal, + TitleBar: application.MacTitleBarHiddenInset, + CollectionBehavior: application.MacWindowCollectionBehaviorFullScreenNone, + } +} + +// LinuxAppearanceOptions is the shared Linux chrome; opaque so fake-translucency compositors paint it. +func LinuxAppearanceOptions(icon []byte) application.LinuxWindow { + return application.LinuxWindow{ + Icon: icon, + WindowIsTranslucent: false, + } +} + +// DialogWindowOptions is the baseline for every auxiliary dialog window; callers override per-dialog. +func DialogWindowOptions(name, title, url string, linuxIcon []byte) application.WebviewWindowOptions { + return application.WebviewWindowOptions{ + Name: name, + Title: title, + Width: 360, + Height: 320, + DisableResize: true, + AlwaysOnTop: true, + Hidden: true, + MinimiseButtonState: application.ButtonHidden, + MaximiseButtonState: application.ButtonHidden, + CloseButtonState: application.ButtonEnabled, + BackgroundColour: WindowBackgroundColour, + URL: url, + Mac: AppleMacOSAppearanceOptions(), + Windows: MicrosoftWindowsAppearanceOptions(), + Linux: LinuxAppearanceOptions(linuxIcon), + } +} + +// WindowManager owns the auxiliary windows (main is created in main.go). Settings is created +// eagerly and hidden on close to keep React state; the rest are created on open, destroyed on +// close, so the macOS dock-reopen handler finds no hidden window to resurrect. +type WindowManager struct { + app *application.App + mainWindow *application.WebviewWindow + translator ErrorTranslator + prefs LanguagePreference + linuxIcon []byte + settings *application.WebviewWindow + browserLogin *application.WebviewWindow + sessionExpiration *application.WebviewWindow + installProgress *application.WebviewWindow + welcome *application.WebviewWindow + errorDialog *application.WebviewWindow + // hiddenForLogin holds windows hidden while the BrowserLogin popup is open, restored on close. + hiddenForLogin []application.Window + mu sync.Mutex + // recenterOnShow is set only on the minimal-WM/XEmbed path, where the WM neither centers nor + // restores position; nil on full desktops so re-centering can't fight a user-moved window. + recenterOnShow func() bool +} + +// NewWindowManager wires the manager to the main app; translator/prefs may be nil (tests). The +// Settings window is created here (hidden) so the first OpenSettings is instant. +func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow, translator ErrorTranslator, prefs LanguagePreference, linuxIcon []byte) *WindowManager { + s := &WindowManager{app: app, mainWindow: mainWindow, translator: translator, prefs: prefs, linuxIcon: linuxIcon} + // Re-title live windows on language flip. Wired internally so the binding generator + // doesn't try to expose the interface param. + if sub, ok := prefs.(LanguageSubscriber); ok && sub != nil { + ch, _ := sub.Subscribe() + go func() { + var last i18n.LanguageCode + for p := range ch { + if p.Language == "" || p.Language == last { + continue + } + last = p.Language + s.retitleAll() + } + }() + } + s.settings = app.Window.NewWithOptions(application.WebviewWindowOptions{ + Name: "settings", + Title: s.title("window.title.settings"), + Width: 900, + Height: WindowHeight, + Hidden: true, + DisableResize: true, + MinimiseButtonState: application.ButtonHidden, + MaximiseButtonState: application.ButtonHidden, + CloseButtonState: application.ButtonEnabled, + BackgroundColour: WindowBackgroundColour, + URL: "/#/settings", + Mac: AppleMacOSAppearanceOptions(), + Windows: MicrosoftWindowsAppearanceOptions(), + Linux: LinuxAppearanceOptions(linuxIcon), + }) + // Hide (not destroy) on close to keep React state; reset to General for a flash-free reopen. + s.settings.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { + e.Cancel() + s.app.Event.Emit(EventSettingsOpen, "general") + s.settings.Hide() + }) + return s +} + +// OpenSettings shows the settings window on tab (empty → General), switching tab via +// EventSettingsOpen rather than SetURL (which would remount the provider tree). +func (s *WindowManager) OpenSettings(tab string) { + target := tab + if target == "" { + target = "general" + } + s.app.Event.Emit(EventSettingsOpen, target) + s.settings.Show() + s.settings.Focus() + // Re-center (minimal-WM only; see centerWhenReady). + s.centerWhenReady(s.settings) +} + +// OpenBrowserLogin shows the SSO popup, creating it on first use. +func (s *WindowManager) OpenBrowserLogin(uri string) { + s.mu.Lock() + defer s.mu.Unlock() + if s.browserLogin == nil { + startURL := "/#/dialog/browser-login" + if uri != "" { + startURL = "/#/dialog/browser-login?uri=" + url.QueryEscape(uri) + } + s.hideOtherWindowsLocked("browser-login") + // Prefer the main window's screen (multi-monitor); falls back to OS-default centering. + var screen *application.Screen + if s.mainWindow != nil { + if sc, err := s.mainWindow.GetScreen(); err == nil { + screen = sc + } + } + opts := DialogWindowOptions("browser-login", s.title("window.title.signIn"), startURL, s.linuxIcon) + // Not always-on-top: it would obscure the browser tab the user logs in through. + opts.AlwaysOnTop = false + opts.InitialPosition = application.WindowCentered + opts.Screen = screen + s.browserLogin = s.app.Window.NewWithOptions(opts) + bl := s.browserLogin + // Red-X close means cancel: emit the event so startLogin() tears down the SSO wait. + bl.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) { + s.app.Event.Emit(EventBrowserLoginCancel) + s.mu.Lock() + s.browserLogin = nil + s.restoreHiddenWindowsLocked() + s.mu.Unlock() + }) + s.centerWhenReady(s.browserLogin) + return + } + if uri != "" { + s.browserLogin.SetURL("/#/dialog/browser-login?uri=" + url.QueryEscape(uri)) + } + s.browserLogin.Show() + s.browserLogin.Focus() + s.centerWhenReady(s.browserLogin) +} + +// BrowserLoginWindow returns the live SSO popup, or nil. While non-nil it is the +// app's focal window: tray "Open" and dock activation hand off to it, not the main window. +func (s *WindowManager) BrowserLoginWindow() *application.WebviewWindow { + s.mu.Lock() + defer s.mu.Unlock() + return s.browserLogin +} + +// InstallProgressWindow returns the live install-progress window, or nil. Same focal-window +// contract as BrowserLoginWindow; install supersedes everything, so check this first. +func (s *WindowManager) InstallProgressWindow() *application.WebviewWindow { + s.mu.Lock() + defer s.mu.Unlock() + return s.installProgress +} + +func (s *WindowManager) CloseBrowserLogin() { + s.mu.Lock() + w := s.browserLogin + s.browserLogin = nil + s.mu.Unlock() + if w != nil { + w.Close() + } +} + +// OpenSessionExpiration shows the countdown warning on the cursor's display; seconds seeds +// the countdown. Singleton, destroyed on close. +func (s *WindowManager) OpenSessionExpiration(seconds int) { + s.mu.Lock() + defer s.mu.Unlock() + startURL := "/#/dialog/session-expiration?seconds=" + strconv.Itoa(seconds) + if s.sessionExpiration == nil { + opts := DialogWindowOptions("session-expiration", s.title("window.title.sessionExpiration"), startURL, s.linuxIcon) + opts.Screen = s.getScreenBasedOnCursorPosition() + opts.InitialPosition = application.WindowCentered + s.sessionExpiration = s.app.Window.NewWithOptions(opts) + s.sessionExpiration.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) { + s.mu.Lock() + s.sessionExpiration = nil + s.mu.Unlock() + }) + s.centerOnCursorScreen(s.sessionExpiration) + return + } + s.sessionExpiration.SetURL(startURL) + s.centerOnCursorScreen(s.sessionExpiration) + s.sessionExpiration.Show() + s.sessionExpiration.Focus() +} + +func (s *WindowManager) CloseSessionExpiration() { + s.mu.Lock() + w := s.sessionExpiration + s.sessionExpiration = nil + s.mu.Unlock() + if w != nil { + w.Close() + } +} + +// OpenInstallProgress shows the install-progress window and hides the rest for the duration +// (restored on close). It owns its own result polling since the daemon restarts mid-install. +func (s *WindowManager) OpenInstallProgress(version string) { + s.mu.Lock() + defer s.mu.Unlock() + startURL := "/#/dialog/install-progress" + if version != "" { + startURL = "/#/dialog/install-progress?version=" + url.QueryEscape(version) + } + if s.installProgress == nil { + s.hideOtherWindowsLocked("install-progress") + s.installProgress = s.app.Window.NewWithOptions( + DialogWindowOptions("install-progress", s.title("window.title.updating"), startURL, s.linuxIcon), + ) + s.installProgress.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) { + s.mu.Lock() + s.installProgress = nil + s.restoreHiddenWindowsLocked() + s.mu.Unlock() + }) + s.centerWhenReady(s.installProgress) + return + } + s.installProgress.SetURL(startURL) + s.installProgress.Show() + s.installProgress.Focus() + s.centerWhenReady(s.installProgress) +} + +func (s *WindowManager) CloseInstallProgress() { + s.mu.Lock() + w := s.installProgress + s.installProgress = nil + s.mu.Unlock() + if w != nil { + w.Close() + } +} + +// OpenWelcome shows the first-launch onboarding window. Singleton, destroyed on close. +func (s *WindowManager) OpenWelcome() { + s.mu.Lock() + defer s.mu.Unlock() + if s.welcome == nil { + opts := DialogWindowOptions("welcome", s.title("window.title.welcome"), "/#/dialog/welcome", s.linuxIcon) + opts.Width = 420 + opts.InitialPosition = application.WindowCentered + s.welcome = s.app.Window.NewWithOptions(opts) + w := s.welcome + w.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) { + s.mu.Lock() + s.welcome = nil + s.mu.Unlock() + }) + s.centerWhenReady(s.welcome) + return + } + s.welcome.Show() + s.welcome.Focus() + s.centerWhenReady(s.welcome) +} + +func (s *WindowManager) CloseWelcome() { + s.mu.Lock() + w := s.welcome + s.welcome = nil + s.mu.Unlock() + if w != nil { + w.Close() + } +} + +// OpenError shows the custom error dialog; title/message are pre-localised and ride in the +// start URL. A second error replaces the open one via SetURL. Singleton, destroyed on close. +func (s *WindowManager) OpenError(title, message string) { + s.mu.Lock() + defer s.mu.Unlock() + startURL := errorDialogURL(title, message) + if s.errorDialog == nil { + s.errorDialog = s.app.Window.NewWithOptions( + DialogWindowOptions("error", s.title("window.title.error"), startURL, s.linuxIcon), + ) + s.errorDialog.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) { + s.mu.Lock() + s.errorDialog = nil + s.mu.Unlock() + }) + s.centerWhenReady(s.errorDialog) + return + } + s.errorDialog.SetURL(startURL) + s.errorDialog.Show() + s.errorDialog.Focus() + s.centerWhenReady(s.errorDialog) +} + +func (s *WindowManager) CloseError() { + s.mu.Lock() + w := s.errorDialog + s.errorDialog = nil + s.mu.Unlock() + if w != nil { + w.Close() + } +} + +// OpenMain brings the main window forward; the welcome handoff uses it instead of the tray. +func (s *WindowManager) OpenMain() { + s.ShowMain() +} + +// ShowMain brings the main window forward (re-centering on minimal WMs). The single entry +// point every surface (tray, SIGUSR1, welcome) should use so centering applies uniformly. +func (s *WindowManager) ShowMain() { + if s.mainWindow == nil { + return + } + s.mainWindow.Show() + s.mainWindow.Focus() + // Re-center (minimal-WM only; see centerWhenReady). + s.centerWhenReady(s.mainWindow) +} + +// SetRecenterOnShow installs the recenterOnShow predicate (see the field). +func (s *WindowManager) SetRecenterOnShow(pred func() bool) { + s.recenterOnShow = pred +} + +// centerWhenReady centers w only on minimal WMs (recenterOnShow); elsewhere it +// returns so it never fights a user-moved window. On GTK4 an inline Center() +// no-ops until the GdkSurface is realized (async, after Show) and InvokeAsync +// would deadlock, so a background goroutine retries until Position is non-zero, +// bounded so a window genuinely at the origin can't spin forever. +func (s *WindowManager) centerWhenReady(w *application.WebviewWindow) { + if w == nil || s.recenterOnShow == nil || !s.recenterOnShow() { + return + } + go func() { + for i := 0; i < 50; i++ { // ~1s budget at 20ms steps + w.Center() + if x, y := w.Position(); x != 0 || y != 0 { + return // surface realized + } + time.Sleep(20 * time.Millisecond) + } + }() +} + +// centerOnCursorScreen centers w on the cursor's display; guards no-op on headless sessions. +// On minimal WMs it uses the same realize-detection retry loop as centerWhenReady. +func (s *WindowManager) centerOnCursorScreen(w *application.WebviewWindow) { + if w == nil { + return + } + place := func() { + screen := s.getScreenBasedOnCursorPosition() + if screen == nil { + return + } + width, height := w.Size() + if width <= 0 || height <= 0 { + return + } + wa := screen.WorkArea + if wa.Width <= 0 || wa.Height <= 0 { + return + } + w.SetPosition(wa.X+(wa.Width-width)/2, wa.Y+(wa.Height-height)/2) + } + place() + if s.recenterOnShow == nil || !s.recenterOnShow() { + return + } + go func() { + for i := 0; i < 50; i++ { + place() + if x, y := w.Position(); x != 0 || y != 0 { + return + } + time.Sleep(20 * time.Millisecond) + } + }() +} + +// title resolves a window-title i18n key in the current language, or the raw key if unavailable. +func (s *WindowManager) title(key string) string { + 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) +} + +// retitleAll re-applies the localised title to every live auxiliary window. Pointers are +// snapshotted under s.mu; SetTitle is then safe to call after releasing the lock. +func (s *WindowManager) retitleAll() { + s.mu.Lock() + type pair struct { + win *application.WebviewWindow + key string + } + wins := []pair{ + {s.settings, "window.title.settings"}, + {s.browserLogin, "window.title.signIn"}, + {s.sessionExpiration, "window.title.sessionExpiration"}, + {s.installProgress, "window.title.updating"}, + {s.welcome, "window.title.welcome"}, + {s.errorDialog, "window.title.error"}, + } + s.mu.Unlock() + for _, p := range wins { + if p.win != nil { + p.win.SetTitle(s.title(p.key)) + } + } +} + +// hideOtherWindowsLocked hides every visible window except keepName, recording +// them in hiddenForLogin for restoreHiddenWindowsLocked. Caller must hold s.mu. +func (s *WindowManager) hideOtherWindowsLocked(keepName string) { + for _, w := range s.app.Window.GetAll() { + if w == nil || w.Name() == keepName { + continue + } + if !w.IsVisible() { + continue + } + w.Hide() + s.hiddenForLogin = append(s.hiddenForLogin, w) + } +} + +// restoreHiddenWindowsLocked re-shows windows hidden by hideOtherWindowsLocked +// (caller holds s.mu). If the main window was among them, raiseToForeground +// lifts it above the SSO browser, which still owns the foreground — a plain +// Show/Focus would be demoted to a taskbar flash and leave it stranded behind. +func (s *WindowManager) restoreHiddenWindowsLocked() { + mainRestored := false + for _, w := range s.hiddenForLogin { + if w == nil { + continue + } + w.Show() + if w == s.mainWindow { + mainRestored = true + } + } + s.hiddenForLogin = nil + if mainRestored && s.mainWindow != nil { + raiseToForeground(s.mainWindow) + } +} + +// getScreenBasedOnCursorPosition returns the cursor's display, falling back to the +// main-window screen, then nil (OS-default placement). +func (s *WindowManager) getScreenBasedOnCursorPosition() *application.Screen { + if s.app == nil || s.app.Screen == nil { + return nil + } + if p, ok := getCursorPosition(s.app); ok { + if sc := s.app.Screen.ScreenNearestDipPoint(p); sc != nil { + return sc + } + } + if s.mainWindow != nil { + if sc, err := s.mainWindow.GetScreen(); err == nil { + return sc + } + } + return nil +} + +// errorDialogURL builds the error window's start URL with title/message as escaped query params. +func errorDialogURL(title, message string) string { + q := url.Values{} + if title != "" { + q.Set("title", title) + } + if message != "" { + q.Set("message", message) + } + startURL := "/#/dialog/error" + if enc := q.Encode(); enc != "" { + startURL += "?" + enc + } + return startURL +} + +// u32ptr returns a pointer to v, for the optional *uint32 Wails theme fields. +func u32ptr(v uint32) *uint32 { return &v } diff --git a/client/ui/signal_unix.go b/client/ui/signal_unix.go index 99de99f0f..876c5dd6e 100644 --- a/client/ui/signal_unix.go +++ b/client/ui/signal_unix.go @@ -1,76 +1,31 @@ -//go:build !windows && !(linux && 386) +//go:build !windows && !android && !ios && !freebsd && !js package main import ( "context" "os" - "os/exec" "os/signal" "syscall" log "github.com/sirupsen/logrus" ) -// setupSignalHandler sets up a signal handler to listen for SIGUSR1. -// When received, it opens the quick actions window. -func (s *serviceClient) setupSignalHandler(ctx context.Context) { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGUSR1) +// listenForShowSignal lets external tools surface the running UI by signalling its pid (SIGUSR1). +func listenForShowSignal(ctx context.Context, tray *Tray) { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGUSR1) go func() { for { select { case <-ctx.Done(): + signal.Stop(sigCh) return - case <-sigChan: - log.Info("received SIGUSR1 signal, opening quick actions window") - s.openQuickActions() + case <-sigCh: + log.Debug("SIGUSR1 received, showing window") + tray.ShowWindow() } } }() } - -// openQuickActions opens the quick actions window by spawning a new process. -func (s *serviceClient) openQuickActions() { - proc, err := os.Executable() - if err != nil { - log.Errorf("get executable path: %v", err) - return - } - - cmd := exec.CommandContext(s.ctx, proc, - "--quick-actions=true", - "--daemon-addr="+s.addr, - ) - - if out := s.attachOutput(cmd); out != nil { - defer func() { - if err := out.Close(); err != nil { - log.Errorf("close log file %s: %v", s.logFile, err) - } - }() - } - - log.Infof("running command: %s --quick-actions=true --daemon-addr=%s", proc, s.addr) - - if err := cmd.Start(); err != nil { - log.Errorf("start quick actions window: %v", err) - return - } - - go func() { - if err := cmd.Wait(); err != nil { - log.Debugf("quick actions window exited: %v", err) - } - }() -} - -// sendShowWindowSignal sends SIGUSR1 to the specified PID. -func sendShowWindowSignal(pid int32) error { - process, err := os.FindProcess(int(pid)) - if err != nil { - return err - } - return process.Signal(syscall.SIGUSR1) -} diff --git a/client/ui/signal_windows.go b/client/ui/signal_windows.go index 58f46374f..86caa6d0c 100644 --- a/client/ui/signal_windows.go +++ b/client/ui/signal_windows.go @@ -5,9 +5,6 @@ package main import ( "context" "errors" - "fmt" - "os" - "os/exec" "time" log "github.com/sirupsen/logrus" @@ -17,155 +14,65 @@ import ( const ( quickActionsTriggerEventName = `Global\NetBirdQuickActionsTriggerEvent` waitTimeout = 5 * time.Second - // SYNCHRONIZE is needed for WaitForSingleObject, EVENT_MODIFY_STATE for ResetEvent. - desiredAccesses = windows.SYNCHRONIZE | windows.EVENT_MODIFY_STATE + desiredAccesses = windows.SYNCHRONIZE | windows.EVENT_MODIFY_STATE + + // WAIT_TIMEOUT return code; not exposed by golang.org/x/sys/windows. + waitTimeoutCode uint32 = 0x00000102 ) -func getEventNameUint16Pointer() (*uint16, error) { - eventNamePtr, err := windows.UTF16PtrFromString(quickActionsTriggerEventName) - if err != nil { - log.Errorf("Failed to convert event name '%s' to UTF16: %v", quickActionsTriggerEventName, err) - return nil, err - } - - return eventNamePtr, nil -} - -// setupSignalHandler sets up signal handling for Windows. -// Windows doesn't support SIGUSR1, so this uses a similar approach using windows.Events. -func (s *serviceClient) setupSignalHandler(ctx context.Context) { - eventNamePtr, err := getEventNameUint16Pointer() +// listenForShowSignal shows the main window when an external process pulses the named event. +func listenForShowSignal(ctx context.Context, tray *Tray) { + namePtr, err := windows.UTF16PtrFromString(quickActionsTriggerEventName) if err != nil { + log.Errorf("trigger event name: %v", err) return } - eventHandle, err := windows.CreateEvent(nil, 1, 0, eventNamePtr) - + handle, err := windows.CreateEvent(nil, 1, 0, namePtr) if err != nil { - if errors.Is(err, windows.ERROR_ALREADY_EXISTS) { - log.Warnf("Quick actions trigger event '%s' already exists. Attempting to open.", quickActionsTriggerEventName) - eventHandle, err = windows.OpenEvent(desiredAccesses, false, eventNamePtr) - if err != nil { - log.Errorf("Failed to open existing quick actions trigger event '%s': %v", quickActionsTriggerEventName, err) - return - } - log.Infof("Successfully opened existing quick actions trigger event '%s'.", quickActionsTriggerEventName) - } else { - log.Errorf("Failed to create quick actions trigger event '%s': %v", quickActionsTriggerEventName, err) + if !errors.Is(err, windows.ERROR_ALREADY_EXISTS) { + log.Errorf("create trigger event %q: %v", quickActionsTriggerEventName, err) + return + } + handle, err = windows.OpenEvent(desiredAccesses, false, namePtr) + if err != nil { + log.Errorf("open trigger event %q: %v", quickActionsTriggerEventName, err) return } } - if eventHandle == windows.InvalidHandle { - log.Errorf("Obtained an invalid handle for quick actions trigger event '%s'", quickActionsTriggerEventName) + if handle == windows.InvalidHandle { + log.Errorf("invalid handle for trigger event %q", quickActionsTriggerEventName) return } - log.Infof("Quick actions handler waiting for signal on event: %s", quickActionsTriggerEventName) - - go s.waitForEvent(ctx, eventHandle) + go waitForTrigger(ctx, handle, tray) } -func (s *serviceClient) waitForEvent(ctx context.Context, eventHandle windows.Handle) { +func waitForTrigger(ctx context.Context, handle windows.Handle, tray *Tray) { defer func() { - if err := windows.CloseHandle(eventHandle); err != nil { - log.Errorf("Failed to close quick actions event handle '%s': %v", quickActionsTriggerEventName, err) + if err := windows.CloseHandle(handle); err != nil { + log.Errorf("close trigger event handle: %v", err) } }() + timeoutMs := uint32(waitTimeout / time.Millisecond) for { if ctx.Err() != nil { return } - - status, err := windows.WaitForSingleObject(eventHandle, uint32(waitTimeout.Milliseconds())) - - switch status { - case windows.WAIT_OBJECT_0: - log.Info("Received signal on quick actions event. Opening quick actions window.") - - // reset the event so it can be triggered again later (manual reset == 1) - if err := windows.ResetEvent(eventHandle); err != nil { - log.Errorf("Failed to reset quick actions event '%s': %v", quickActionsTriggerEventName, err) - } - - s.openQuickActions() - case uint32(windows.WAIT_TIMEOUT): - - default: - if isDone := logUnexpectedStatus(ctx, status, err); isDone { - return + ev, err := windows.WaitForSingleObject(handle, timeoutMs) + switch { + case err != nil: + log.Errorf("wait trigger event: %v", err) + return + case ev == waitTimeoutCode: + continue + case ev == windows.WAIT_OBJECT_0: + if err := windows.ResetEvent(handle); err != nil { + log.Errorf("reset trigger event: %v", err) } + tray.ShowWindow() } } } - -func logUnexpectedStatus(ctx context.Context, status uint32, err error) bool { - log.Errorf("Unexpected status %d from WaitForSingleObject for quick actions event '%s': %v", - status, quickActionsTriggerEventName, err) - select { - case <-time.After(5 * time.Second): - return false - case <-ctx.Done(): - return true - } -} - -// openQuickActions opens the quick actions window by spawning a new process. -func (s *serviceClient) openQuickActions() { - proc, err := os.Executable() - if err != nil { - log.Errorf("get executable path: %v", err) - return - } - - cmd := exec.CommandContext(s.ctx, proc, - "--quick-actions=true", - "--daemon-addr="+s.addr, - ) - - if out := s.attachOutput(cmd); out != nil { - defer func() { - if err := out.Close(); err != nil { - log.Errorf("close log file %s: %v", s.logFile, err) - } - }() - } - - log.Infof("running command: %s --quick-actions=true --daemon-addr=%s", proc, s.addr) - - if err := cmd.Start(); err != nil { - log.Errorf("error starting quick actions window: %v", err) - return - } - - go func() { - if err := cmd.Wait(); err != nil { - log.Debugf("quick actions window exited: %v", err) - } - }() -} - -func sendShowWindowSignal(pid int32) error { - _, err := os.FindProcess(int(pid)) - if err != nil { - return err - } - - eventNamePtr, err := getEventNameUint16Pointer() - if err != nil { - return err - } - - eventHandle, err := windows.OpenEvent(desiredAccesses, false, eventNamePtr) - if err != nil { - return err - } - - err = windows.SetEvent(eventHandle) - if err != nil { - return fmt.Errorf("error setting event: %w", err) - } - - return nil -} diff --git a/client/ui/tray.go b/client/ui/tray.go new file mode 100644 index 000000000..700d94098 --- /dev/null +++ b/client/ui/tray.go @@ -0,0 +1,531 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "context" + "runtime" + "strings" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" + "github.com/wailsapp/wails/v3/pkg/services/notifications" + + "github.com/netbirdio/netbird/client/ui/authsession" + "github.com/netbirdio/netbird/client/ui/i18n" + "github.com/netbirdio/netbird/client/ui/services" + "github.com/netbirdio/netbird/version" +) + +// Notification IDs are OS dedup keys that coalesce duplicate toasts; +// statusError is a tray-only sentinel for the error-icon state. +const ( + notifyIDUpdatePrefix = "netbird-update-" + notifyIDEvent = "netbird-event-" + notifyIDTrayError = "netbird-tray-error" + notifyIDMDMPolicy = "netbird-mdm-policy" + + statusError = "Error" + + urlGitHubRepo = "https://github.com/netbirdio/netbird" + urlGitHubReleases = "https://github.com/netbirdio/netbird/releases/latest" + urlDocs = "https://docs.netbird.io" +) + +// TrayServices bundles the services the tray menu needs, grouped so NewTray +// stays under the linter's parameter-count threshold. +type TrayServices struct { + Connection *services.Connection + Settings *services.Settings + Profiles *services.Profiles + Networks *services.Networks + DaemonFeed *services.DaemonFeed + Notifier *notifications.NotificationService + Update *services.Update + ProfileSwitcher *services.ProfileSwitcher + WindowManager *services.WindowManager + // Session is bound to authsession directly because the services wrapper + // only re-exposes the React subset. + Session *authsession.Session + Localizer *Localizer +} + +type Tray struct { + app *application.App + tray *application.SystemTray + window *application.WebviewWindow + svc TrayServices + // panelDark reports whether the desktop panel uses a dark scheme, so + // iconForState can pick the black vs white mono tray icon on Linux. Set + // by startTrayTheme (Linux only); nil elsewhere, where panelIsDark falls + // back to its default. + panelDark func() bool + loc *Localizer + + // menu and the *Item/*Submenu fields below are reassigned by buildMenu + // on every relayout — touch them only with menuMu held. Exceptions: + // the Connect/Disconnect OnClick closures capture their own item, and + // refreshSessionExpiresLabel snapshots its item under menuMu. + menu *application.Menu + statusItem *application.MenuItem + // sessionExpiresItem shows the SSO deadline as a remaining-time label, + // repainted by a 30s ticker. + sessionExpiresItem *application.MenuItem + upItem *application.MenuItem + downItem *application.MenuItem + exitNodeItem *application.MenuItem + exitNodeSubmenu *application.Menu + profileSubmenu *application.Menu + profileSubmenuItem *application.MenuItem + profileEmailItem *application.MenuItem + settingsItem *application.MenuItem + daemonVersionItem *application.MenuItem + + updater *trayUpdater + + // statusMu guards the daemon-status core mirrored on the tray. One mutex + // covers these fields because applyStatus writes them together on every + // Status push and the menu painters read them. + statusMu sync.Mutex + connected bool + lastStatus string + lastDaemonVersion string + // lastNetworksRevision is the daemon's routed-networks revision; a bump (or + // a connect/disconnect transition) gates the refreshExitNodes re-fetch so + // ListNetworks runs only when routes change. The peer-status route list + // can't substitute: it carries only actively-routed routes, not candidate + // exit nodes. + lastNetworksRevision uint64 + // pendingConnectLogin is set when handleConnect fires an Up on an idle + // daemon. The daemon flips to NeedsLogin if the peer is SSO-tracked with + // no cached token; applyStatus consumes the flag on that transition to + // open the browser-login flow, saving a second Connect click. + // Profile-switch reconnects are handled separately by + // DaemonFeed.statusStreamLoop. + pendingConnectLogin bool + + // sessionMu guards the cached SSO deadline used by the session row. + // Independent of statusMu so the 30s ticker reader and the Status-push + // writer don't block each other. + sessionMu sync.Mutex + sessionExpiresAt time.Time + + // profileMu guards the profile-domain state (active identity, the + // notifications gate, the in-flight switch cancel). Independent of + // statusMu so a long-running switch holding switchCancel doesn't block a + // Status-push reader of t.connected. + profileMu sync.Mutex + activeProfile string + activeUsername string + notificationsEnabled bool + switchCancel context.CancelFunc + + // profileLoadMu serializes loadProfiles so the applyStatus refresh can't + // race the ApplicationStarted seed or the post-switch reload — all + // manipulate profileSubmenu + SetMenu, which Wails isn't concurrency-safe + // against. + profileLoadMu sync.Mutex + + // profilesMu guards the cached profile rows that relayoutMenu repaints + // into a freshly built Profiles submenu, kept separate from the live + // submenu so a relayout always has a source to repaint from without + // re-hitting the daemon. + profilesMu sync.Mutex + profiles []services.Profile + profilesUser string + + // menuMu serialises relayoutMenu (buildMenu + SetMenu) and guards the + // menu/item-pointer fields above. relayoutMenu is the only post-startup + // SetMenu call site — a menu snapshot pushed outside the lock could + // reinstall a stale tree. + menuMu sync.Mutex + + // exitNodesMu guards the exitNodes row cache so relayoutMenu's read (and + // the Repaint copy) doesn't contend with status-push readers of statusMu. + exitNodesMu sync.Mutex + exitNodes []exitNodeEntry + // exitNodesRebuildMu serialises the ListNetworks fetch + submenu rebuild + + // SetMenu cycle so back-to-back Status pushes can't run it concurrently + // with itself. + exitNodesRebuildMu sync.Mutex + + // featureMu guards the daemon feature kill switches mirrored on the tray. + // Fetched at startup and refreshed on every config_changed event (the + // daemon re-applies MDM policy per engine spawn), so featuresDisabled can + // grey out menus without polling GetFeatures. + featureMu sync.Mutex + disableProfiles bool + disableNetworks bool +} + +func NewTray(app *application.App, window *application.WebviewWindow, svc TrayServices) *Tray { + t := &Tray{ + app: app, + window: window, + svc: svc, + notificationsEnabled: true, + // Localizer is constructed by main so the first menu render is already + // in the right locale — no English flash then re-paint. + loc: svc.Localizer, + } + t.updater = newTrayUpdater(app, window, svc.Update, svc.Notifier, t.loc, func() { t.applyIcon() }, func() { t.relayoutMenu() }) + t.tray = app.SystemTray.New() + // Seed panel-theme detection before the first paint so the initial icon + // matches the panel's light/dark scheme (Linux only). + t.startTrayTheme() + t.applyIcon() + t.tray.SetTooltip(t.loc.T("tray.tooltip")) + // On Linux the SNI hover tooltip rides on the systray label, not + // SetTooltip (a no-op there); without a label Wails shows the literal + // "Wails". macOS/Windows are skipped (label paints visible text on + // macOS; Windows uses SetTooltip above). + if runtime.GOOS == "linux" { + t.tray.SetLabel(t.loc.T("tray.tooltip")) + } + t.menu = t.buildMenu() + t.tray.SetMenu(t.menu) + // macOS/Linux give click→menu natively, so bindTrayClick is a no-op there + // (binding OnClick→OpenMenu on macOS would freeze the tray); Windows has no + // native left-click handler so it wires one to open the main window, leaving + // the menu on right-click (see tray_click_*.go). On Linux AttachWindow is + // skipped — with applySmartDefaults it would pop the window alongside the + // menu (e.g. GNOME Shell AppIndicator). + bindTrayClick(t) + + app.Event.On(services.EventStatusSnapshot, t.onStatusEvent) + app.Event.On(services.EventDaemonNotification, t.onSystemEvent) + // Refresh the Profiles submenu on ProfileSwitcher's change event. A + // switch on an idle daemon drives no status transition, so without this + // hook a React-initiated switch leaves the tray's submenu stale. + app.Event.On(services.EventProfileChanged, func(*application.CustomEvent) { + go t.loadProfiles() + }) + // Defer the first profile load until the menu impl is live — Menu.Update() + // short-circuits while app.running is false, and AppKit's main queue isn't + // ready earlier (see d23ef34 InvokeSync nil-deref). + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + go t.loadProfiles() + go t.refreshRestrictions() + go t.runSessionExpiryTicker() + // Category registration must run after the notifications service + // Startup populates appName/registry path on Windows; before app.Run() + // the category lookup silently falls back to a plain notification. + t.registerSessionWarningCategory() + }) + + t.loc.Watch(func(i18n.LanguageCode) { t.applyLanguage() }) + + go t.loadConfig() + return t +} + +// ShowWindow brings the main window forward — used by SIGUSR1 / Windows event. +// Show() alone is not enough on macOS (makeKeyAndOrderFront skips activation, +// so the window pops up behind the active app); Focus() additionally calls +// activateIgnoringOtherApps:YES on macOS and SetForegroundWindow on Windows. +func (t *Tray) ShowWindow() { + // An install supersedes every other flow, so check it before BrowserLogin. + if w := t.svc.WindowManager.InstallProgressWindow(); w != nil { + w.Show() + w.Focus() + return + } + if w := t.svc.WindowManager.BrowserLoginWindow(); w != nil { + w.Show() + w.Focus() + return + } + if t.window == nil { + return + } + // Route through WindowManager so the main window is centered on first + // show — minimal WMs (fluxbox, the XEmbed tray path) otherwise drop it in + // the top-left corner. + if t.svc.WindowManager != nil { + t.svc.WindowManager.ShowMain() + return + } + t.window.Show() + t.window.Focus() +} + +// applyLanguage re-renders every translated surface in the Localizer's current +// language. Wails dispatches menu/tray APIs onto the UI thread internally, so +// calling them from the Localizer's background goroutine is safe; profileLoadMu +// prevents loadProfiles from racing the rebuild. +func (t *Tray) applyLanguage() { + t.tray.SetTooltip(t.loc.T("tray.tooltip")) + // Mirror the Linux label fix from NewTray (the SNI tooltip rides on the + // label). + if runtime.GOOS == "linux" { + t.tray.SetLabel(t.loc.T("tray.tooltip")) + } + t.relayoutMenu() +} + +// relayoutMenu rebuilds the entire tray menu, repaints the cached +// status/session/profile/exit-node state into the fresh items, and pushes the +// whole tree with a single SetMenu. +// +// A full rebuild is required because on KDE/Plasma the StatusNotifierItem host +// caches a submenu's layout on first open (GetLayout for that submenu id) and +// never re-fetches it on a LayoutUpdated(parent=0) signal — so Clear()+Add() +// into the same container froze both the visible rows and the click→id mapping, +// and stale ids no-op'd. buildMenu allocates a fresh submenu container id each +// time, which Plasma treats as unseen and re-queries (confirmed via +// dbus-monitor). This also covers the darwin detached-NSMenu workaround, since +// it rebuilds the whole tree against the cached top-level pointer. +// +// Rows come from the profilesMu/exitNodes caches, so it never re-hits the +// daemon or recurses back into loadProfiles. +func (t *Tray) relayoutMenu() { + t.menuMu.Lock() + defer t.menuMu.Unlock() + + t.menu = t.buildMenu() + + t.statusMu.Lock() + connected := t.connected + lastStatus := t.lastStatus + daemonVersion := t.lastDaemonVersion + t.statusMu.Unlock() + + t.sessionMu.Lock() + sessionDeadline := t.sessionExpiresAt + t.sessionMu.Unlock() + + t.exitNodesMu.Lock() + exitNodeEntries := append([]exitNodeEntry(nil), t.exitNodes...) + t.exitNodesMu.Unlock() + + disableProfiles, disableNetworks := t.featuresDisabled() + + daemonUnavailable := strings.EqualFold(lastStatus, services.StatusDaemonUnavailable) + connecting := strings.EqualFold(lastStatus, services.StatusConnecting) + + if t.statusItem != nil && lastStatus != "" { + t.statusItem.SetLabel(t.loc.StatusLabel(lastStatus)) + t.statusItem.SetEnabled(statusRowEnabled()) + t.applyStatusIndicator(lastStatus) + } + if t.sessionExpiresItem != nil { + if sessionDeadline.IsZero() { + t.sessionExpiresItem.SetHidden(true) + } else { + remaining := t.formatSessionRemaining(time.Until(sessionDeadline)) + t.sessionExpiresItem.SetLabel(t.loc.T("tray.session.expiresIn", "remaining", remaining)) + t.sessionExpiresItem.SetHidden(false) + } + } + if t.upItem != nil { + // Connect stays visible in the NeedsLogin states too — Up drives + // the SSO re-auth flow; hidden only when it would be a no-op. + t.upItem.SetHidden(connected || connecting || daemonUnavailable) + t.upItem.SetEnabled(!connected && !connecting && !daemonUnavailable) + } + if t.downItem != nil { + // Disconnect doubles as the Connecting abort path. + t.downItem.SetHidden(!connected && !connecting) + t.downItem.SetEnabled(connected || connecting) + } + if t.exitNodeItem != nil { + t.exitNodeItem.SetEnabled(connected && len(exitNodeEntries) > 0 && !disableNetworks) + } + if t.settingsItem != nil { + t.settingsItem.SetEnabled(!daemonUnavailable) + } + if t.profileSubmenuItem != nil { + t.profileSubmenuItem.SetEnabled(!daemonUnavailable && !disableProfiles) + } + if daemonVersion != "" && t.daemonVersionItem != nil { + t.daemonVersionItem.SetLabel(t.loc.T("tray.menu.daemonVersion", "version", daemonVersion)) + } + if t.updater != nil { + t.updater.applyLanguage() + } + // buildMenu recreated empty submenus, so repaint both from their caches + // before SetMenu. Neither fill re-fetches. Do NOT re-take + // exitNodesRebuildMu here — refreshExitNodes already holds it when it + // calls relayoutMenu. + t.fillExitNodeSubmenu(exitNodeEntries) + t.fillProfileSubmenu() + + // Single push of the whole tree: on Linux one LayoutUpdated with fresh + // container ids; on darwin an NSMenu rebuild against the cached pointer. + t.tray.SetMenu(t.menu) +} + +func (t *Tray) buildMenu() *application.Menu { + menu := application.NewMenu() + + // Enabled state is platform-dependent (see statusRowEnabled): Windows keeps + // it enabled because the disabled mask would desaturate the coloured status + // dot; macOS/Linux disable it so the greyed label signals it isn't + // clickable. + t.statusItem = menu.Add(t.loc.T("tray.status.disconnected")). + SetEnabled(statusRowEnabled()). + SetBitmap(iconMenuDotIdle) + + menu.AddSeparator() + + // The OnClick closures capture the local item because t.upItem/t.downItem + // are menuMu-guarded and must not be read from the click goroutine. + upItem := menu.Add(t.loc.T("tray.menu.connect")) + upItem.OnClick(func(*application.Context) { t.handleConnect(upItem) }) + t.upItem = upItem + downItem := menu.Add(t.loc.T("tray.menu.disconnect")) + downItem.OnClick(func(*application.Context) { t.handleDisconnect(downItem) }) + downItem.SetHidden(true) + t.downItem = downItem + + menu.AddSeparator() + + // Populated asynchronously once the app has started — Menu.Update() is a + // no-op before app.running is true, so the initial fill is gated on the + // ApplicationStarted hook. + profilesLabel := t.loc.T("tray.menu.profiles") + t.profileSubmenu = menu.AddSubmenu(profilesLabel) + // AddSubmenu returns the child *Menu, so retrieve the parent *MenuItem via + // FindByLabel. + t.profileSubmenuItem = menu.FindByLabel(profilesLabel) + t.profileEmailItem = menu.Add("").SetEnabled(false) + t.profileEmailItem.SetHidden(true) + // Click opens the SessionExpiration window so the user can extend ahead of + // the daemon's T-FinalWarningLead auto-prompt. + t.sessionExpiresItem = menu.Add("").OnClick(func(*application.Context) { t.openSessionExtendFlow() }) + t.sessionExpiresItem.SetHidden(true) + + menu.AddSeparator() + // Accelerators on the Settings/Quit entries below are a no-op on Windows in + // Wails v3 alpha.95 (impl commented out in menuitem_windows.go); still set + // for forward compatibility. macOS/GTK render and fire them. + menu.Add(t.loc.T("tray.menu.open")).OnClick(func(*application.Context) { t.ShowWindow() }) + + menu.AddSeparator() + + // exitNodeSubmenu hosts one row per peer advertising a default route + // (0.0.0.0/0 or ::/0). FindByLabel grabs the parent so applyStatus can flip + // its enabled state independently of the children. + exitNodeLabel := t.loc.T("tray.menu.exitNode") + t.exitNodeSubmenu = menu.AddSubmenu(exitNodeLabel) + t.exitNodeItem = menu.FindByLabel(exitNodeLabel) + t.exitNodeItem.SetEnabled(false) + + menu.AddSeparator() + + // The label's trailing ellipsis follows the macOS HIG convention for items + // that open a window. + t.settingsItem = menu.Add(t.loc.T("tray.menu.settings")). + SetAccelerator("CmdOrCtrl+,"). + OnClick(func(*application.Context) { t.svc.WindowManager.OpenSettings("") }) + + aboutLabel := menuLabel(t.loc.T("tray.menu.about")) + about := menu.AddSubmenu(aboutLabel) + about.Add(t.loc.T("tray.menu.github")).OnClick(func(*application.Context) { + _ = t.app.Browser.OpenURL(urlGitHubRepo) + }) + about.Add(t.loc.T("tray.menu.documentation")).OnClick(func(*application.Context) { + _ = t.app.Browser.OpenURL(urlDocs) + }) + about.Add(t.loc.T("tray.menu.troubleshoot")).OnClick(func(*application.Context) { + t.svc.WindowManager.OpenSettings("troubleshooting") + }) + about.AddSeparator() + about.Add(t.loc.T("tray.menu.guiVersion", "version", version.NetbirdVersion())).SetEnabled(false) + t.daemonVersionItem = about.Add(t.loc.T("tray.menu.daemonVersion", "version", t.loc.T("tray.menu.versionUnknown"))).SetEnabled(false) + // trayUpdater rewrites the label between downloadLatest (opt-in) and + // installVersion (enforced) and drives the click. + updateItem := about.Add(t.loc.T("tray.menu.downloadLatest")). + OnClick(func(*application.Context) { t.updater.handleClick() }) + updateItem.SetHidden(true) + t.updater.attach(updateItem) + + menu.AddSeparator() + menu.Add(t.loc.T("tray.menu.quit")). + SetAccelerator("CmdOrCtrl+Q"). + OnClick(func(*application.Context) { t.app.Quit() }) + + return menu +} + +// handleConnect receives the clicked item from the buildMenu closure — +// t.upItem is menuMu-guarded and must not be read here. +func (t *Tray) handleConnect(upItem *application.MenuItem) { + // NeedsLogin/SessionExpired/LoginFailed won't honor a plain Up RPC — they + // need the Login → WaitSSOLogin → Up sequence. Emit EventTriggerLogin so + // the React startLogin() (which owns the BrowserLogin popup) drives it; + // the hidden main webview is alive and subscribed, so only the popup shows. + t.statusMu.Lock() + needsLogin := strings.EqualFold(t.lastStatus, services.StatusNeedsLogin) || + strings.EqualFold(t.lastStatus, services.StatusSessionExpired) || + strings.EqualFold(t.lastStatus, services.StatusLoginFailed) + t.statusMu.Unlock() + if needsLogin { + t.app.Event.Emit(services.EventTriggerLogin) + return + } + upItem.SetEnabled(false) + // Arm the SSO auto-handoff: Up() is async and the daemon may flip to + // NeedsLogin on an SSO peer with no cached token. applyStatus consumes the + // flag on that transition to trigger browser-login without a second Connect + // click, and clears it on any terminal state. + t.statusMu.Lock() + t.pendingConnectLogin = true + t.statusMu.Unlock() + go func() { + if err := t.svc.Connection.Up(context.Background(), services.UpParams{}); err != nil { + log.Errorf("connect: %v", err) + t.notifyError(t.loc.T("notify.error.connect")) + t.statusMu.Lock() + t.pendingConnectLogin = false + t.statusMu.Unlock() + upItem.SetEnabled(true) + } + }() +} + +// handleDisconnect aborts any in-flight profile switch before sending Down — +// otherwise the switcher's queued Up would reconnect right after, making the +// click a no-op. Also clears Peers' optimistic-Connecting guard so the daemon's +// Idle push paints through instead of being swallowed by the suppression filter. +// Receives the clicked item from the buildMenu closure (see handleConnect). +func (t *Tray) handleDisconnect(downItem *application.MenuItem) { + downItem.SetEnabled(false) + t.profileMu.Lock() + if t.switchCancel != nil { + t.switchCancel() + t.switchCancel = nil + } + t.profileMu.Unlock() + t.svc.DaemonFeed.CancelProfileSwitch() + go func() { + if err := t.svc.Connection.Down(context.Background()); err != nil { + log.Errorf("disconnect: %v", err) + t.notifyError(t.loc.T("notify.error.disconnect")) + downItem.SetEnabled(true) + } + }() +} + +// notify wraps the Wails notification service with the tray's standard +// id-prefix scheme and swallows errors (notifications are best-effort). +func (t *Tray) notify(title, body, id string) { + if t.svc.Notifier == nil { + return + } + _ = safeSendNotification(t.svc.Notifier.SendNotification, title, notifications.NotificationOptions{ + ID: id, + Title: title, + Body: body, + }) +} + +// notifyError fires a generic "Error" notification for tray-driven action +// failures. Each tray click site already logs the underlying error; this +// adds the user-visible toast. +func (t *Tray) notifyError(message string) { + t.notify(t.loc.T("notify.error.title"), message, notifyIDTrayError) +} diff --git a/client/ui/tray_click_linux.go b/client/ui/tray_click_linux.go new file mode 100644 index 000000000..34a364fd9 --- /dev/null +++ b/client/ui/tray_click_linux.go @@ -0,0 +1,31 @@ +//go:build linux && !(linux && 386) + +package main + +// bindTrayClick wires the tray icon's left-click handler on Linux. +// +// Both Linux click paths converge on Wails' linuxSystemTray.Activate, which +// fires the registered clickHandler: +// - Real SNI hosts (KDE Plasma, Waybar, GNOME Shell + AppIndicator) invoke +// org.kde.StatusNotifierItem.Activate over D-Bus on left-click. +// - The in-process StatusNotifierWatcher + XEmbed host used on minimal WMs +// (Fluxbox, i3, dwm, OpenBox) maps a Button1 press to that same Activate +// call itself (xembed_host_linux.go), so it routes through the same hook. +// Registering OnClick here therefore covers both paths with one handler — no +// changes to the watcher or XEmbed C code are needed. Left-click now opens the +// main window; right-click still opens the menu via Wails' default +// SecondaryActivate→OpenMenu handler (and the XEmbed GTK popup on minimal WMs). +// +// We do NOT register OnDoubleClick: Wails' Linux SNI backend never fires it +// (unlike Windows). And we deliberately skip AttachWindow — it plus Wails3's +// applySmartDefaults would pop the window alongside the menu on GNOME Shell +// with the AppIndicator extension (see the bindTrayClick comment in tray.go). +// +// ShowWindow() is the same dispatcher the explicit "Open NetBird" menu entry +// and SIGUSR1 use: it brings the install-progress / browser-login window +// forward when one of those flows is active, otherwise routes through +// WindowManager.ShowMain so the window re-centers on minimal WMs / the XEmbed +// path instead of landing in the top-left corner. +func bindTrayClick(t *Tray) { + t.tray.OnClick(func() { t.ShowWindow() }) +} diff --git a/client/ui/tray_click_other.go b/client/ui/tray_click_other.go new file mode 100644 index 000000000..e6a29e419 --- /dev/null +++ b/client/ui/tray_click_other.go @@ -0,0 +1,13 @@ +//go:build !windows && !android && !ios && !freebsd && !js && (!linux || (linux && 386)) + +package main + +func bindTrayClick(*Tray) { + // No-op: macOS's native NSStatusItem opens the menu on click itself, and + // binding OnClick→anything blocking there froze the tray historically + // (see tray_click_windows.go). Windows wires an explicit handler + // (tray_click_windows.go); Linux opens the window on left-click + // (tray_click_linux.go). The (linux && 386) arm keeps a no-op fallback for + // the i386 Linux build, which excludes the cgo XEmbed/SNI files that + // tray_click_linux.go's build tag matches. +} diff --git a/client/ui/tray_click_windows.go b/client/ui/tray_click_windows.go new file mode 100644 index 000000000..17a6dc5df --- /dev/null +++ b/client/ui/tray_click_windows.go @@ -0,0 +1,8 @@ +//go:build windows + +package main + +// Open application window on left click, right click opens the tray menu +func bindTrayClick(t *Tray) { + t.tray.OnClick(func() { t.ShowWindow() }) +} diff --git a/client/ui/tray_events.go b/client/ui/tray_events.go new file mode 100644 index 000000000..12da68a5c --- /dev/null +++ b/client/ui/tray_events.go @@ -0,0 +1,137 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "fmt" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/wailsapp/wails/v3/pkg/application" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/client/ui/authsession" + "github.com/netbirdio/netbird/client/ui/services" +) + +// onSystemEvent fires an OS notification for daemon SystemEvents that carry a +// user-facing message. Gated by the "Notifications" toggle; critical events bypass it. +func (t *Tray) onSystemEvent(ev *application.CustomEvent) { + se, ok := ev.Data.(services.SystemEvent) + if !ok { + return + } + // config_changed carries no UserMessage, so handle it before the message gate below. + if se.Category == "system" && se.Metadata[proto.MetadataTypeKey] == proto.MetadataTypeConfigChanged { + log.Infof("config_changed event received (source=%s); refreshing tray restrictions", se.Metadata[proto.MetadataSourceKey]) + go t.refreshRestrictions() + go t.loadConfig() + // MDM gets a localised toast here; the daemon's English "policy_applied" + // event is suppressed in shouldSkipSystemEvent. Other sources stay silent. + if se.Metadata[proto.MetadataSourceKey] == proto.MetadataSourceMDM { + t.profileMu.Lock() + enabled := t.notificationsEnabled + t.profileMu.Unlock() + if enabled { + t.notify( + t.loc.T("notify.mdm.policyApplied.title"), + t.loc.T("notify.mdm.policyApplied.body"), + notifyIDMDMPolicy, + ) + } + } + return + } + // Session-warning and deadline-rejected events build their body locally from + // metadata; every other event needs a UserMessage. + isSessionWarning := se.Metadata[authsession.MetaWarning] == "true" + isDeadlineRejected := se.Metadata[authsession.MetaDeadlineRejected] != "" + if !isSessionWarning && !isDeadlineRejected && se.UserMessage == "" { + return + } + if shouldSkipSystemEvent(se) { + return + } + + critical := strings.EqualFold(se.Severity, services.SeverityCritical) + t.profileMu.Lock() + enabled := t.notificationsEnabled + t.profileMu.Unlock() + if !enabled && !critical { + return + } + + // Session-warning events route via stable metadata flags rather than + // category/severity so a daemon-side reword still lands here. Final warning + // auto-opens the SessionExpiration dialog with no notification (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.openSessionExpiration() + return + } + t.notifySessionWarning( + t.loc.T("notify.sessionWarning.title"), + t.buildSessionWarningBody(se.Metadata), + ) + return + } + + body := se.UserMessage + if id := se.Metadata["id"]; id != "" { + body += fmt.Sprintf(" ID: %s", id) + } + t.notify(eventTitle(se), body, notifyIDEvent+se.ID) +} + +// eventTitle composes a notification title, e.g. "Critical: DNS", "Warning: Authentication". +func eventTitle(e services.SystemEvent) string { + prefix := titleCase(e.Severity) + if prefix == "" { + prefix = "Info" + } + category := titleCase(e.Category) + if category == "" { + category = "System" + } + return prefix + ": " + category +} + +func titleCase(s string) string { + if s == "" { + return "" + } + return strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) +} + +// shouldSkipSystemEvent reports whether a daemon SystemEvent must not surface as +// a tray notification: +// - update-available announcements (trayUpdater emits its own) +// - install-progress signals (consumed by the install-progress window) +// - the ::/0 partner of an exit-node default route (0.0.0.0/0 already toasted) +func shouldSkipSystemEvent(se services.SystemEvent) bool { + // "policy_applied" carries a hardcoded English message; the localised toast + // fires on the paired config_changed (source=mdm) event instead. + if se.Metadata[proto.MetadataTypeKey] == proto.MetadataTypePolicyApplied { + return true + } + if _, isUpdate := se.Metadata["new_version_available"]; isUpdate { + return true + } + if _, isProgress := se.Metadata["progress_window"]; isProgress { + return true + } + if se.Category == "network" && se.Metadata["network"] == "::/0" { + return true + } + return false +} diff --git a/client/ui/tray_exitnodes.go b/client/ui/tray_exitnodes.go new file mode 100644 index 000000000..3e6f30842 --- /dev/null +++ b/client/ui/tray_exitnodes.go @@ -0,0 +1,148 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "context" + "net/netip" + "sort" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "github.com/wailsapp/wails/v3/pkg/application" + + "github.com/netbirdio/netbird/client/ui/services" +) + +// exitNodeEntry is one Exit Node submenu row; ID is the network's NetID, the Select/Deselect argument. +type exitNodeEntry struct { + ID string + Selected bool +} + +// fillExitNodeSubmenu uses a "✓ " prefix with plain Add, not AddCheckbox: Wails +// auto-toggles a checkbox on click before OnClick runs, so the deselect/select +// round-trip would briefly show two checked rows. Callers must hold exitNodesRebuildMu. +func (t *Tray) fillExitNodeSubmenu(nodes []exitNodeEntry) { + if t.exitNodeSubmenu == nil { + return + } + t.exitNodeSubmenu.Clear() + for _, n := range nodes { + id := n.ID + selected := n.Selected + label := id + if selected { + label = "✓ " + id + } + t.exitNodeSubmenu.Add(label).OnClick(func(*application.Context) { + t.toggleExitNode(id, selected) + }) + } +} + +// refreshExitNodes sources rows from Networks.List() rather than the Status stream +// because only ListNetworks carries the NetID + selected state Select/Deselect need. +// Serialized by exitNodesRebuildMu against overlapping Status pushes. +func (t *Tray) refreshExitNodes() { + t.exitNodesRebuildMu.Lock() + defer t.exitNodesRebuildMu.Unlock() + + t.statusMu.Lock() + connected := t.connected + t.statusMu.Unlock() + + var nodes []exitNodeEntry + if connected { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + list, err := t.svc.Networks.List(ctx) + cancel() + if err != nil { + log.Debugf("tray list networks: %v", err) + return + } + nodes = exitNodesFromNetworks(list) + } + + log.Infof("tray refreshExitNodes: %d exit node(s)", len(nodes)) + for _, n := range nodes { + log.Infof("tray exit node: id=%q selected=%v", n.ID, n.Selected) + } + + t.exitNodesMu.Lock() + changed := !equalExitNodes(nodes, t.exitNodes) + t.exitNodes = nodes + t.exitNodesMu.Unlock() + + // relayoutMenu repaints from the cached entries, so the old exitNodeItem needs no poking here. + if changed { + t.relayoutMenu() + } +} + +// toggleExitNode uses append=true: append=false would drop the whole current +// selection (default-on semantics), turning off every other routed network the +// user had enabled. Mutual exclusion of exit nodes is enforced daemon-side. +func (t *Tray) toggleExitNode(id string, selected bool) { + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + params := services.SelectNetworksParams{NetworkIDs: []string{id}, Append: true, All: false} + var err error + if selected { + err = t.svc.Networks.Deselect(ctx, params) + } else { + err = t.svc.Networks.Select(ctx, params) + } + if err != nil { + log.Errorf("tray toggle exit node %q: %v", id, err) + t.notifyError(t.loc.T("notify.error.exitNode", "name", id)) + return + } + t.refreshExitNodes() + }() +} + +// exitNodesFromNetworks keeps only networks whose range is a default route: those are the exit-node candidates. +func exitNodesFromNetworks(networks []services.Network) []exitNodeEntry { + out := []exitNodeEntry{} + for _, n := range networks { + if !rangeIsDefaultRoute(n.Range) { + continue + } + out = append(out, exitNodeEntry{ID: n.ID, Selected: n.Selected}) + } + sort.Slice(out, func(i, j int) bool { + return strings.ToLower(out[i].ID) < strings.ToLower(out[j].ID) + }) + return out +} + +// rangeIsDefaultRoute reports whether r contains a default route. The daemon may +// comma-join a v4+v6 pair ("0.0.0.0/0, ::/0"), so each part is parsed rather than string-compared. +func rangeIsDefaultRoute(r string) bool { + for _, part := range strings.Split(r, ",") { + pref, err := netip.ParsePrefix(strings.TrimSpace(part)) + if err != nil { + continue + } + if pref.Bits() == 0 && pref.Addr().IsUnspecified() { + return true + } + } + return false +} + +func equalExitNodes(a, b []exitNodeEntry) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/client/ui/tray_features.go b/client/ui/tray_features.go new file mode 100644 index 000000000..2ff99e2be --- /dev/null +++ b/client/ui/tray_features.go @@ -0,0 +1,37 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "context" + + log "github.com/sirupsen/logrus" +) + +// refreshRestrictions re-reads the operator-disabled UI flags and re-gates the +// menu. Must run on every config_changed event: the daemon re-applies its MDM +// policy on each engine spawn. +func (t *Tray) refreshRestrictions() { + r, err := t.svc.Settings.GetRestrictions(context.Background()) + if err != nil { + log.Debugf("get restrictions: %v", err) + return + } + t.featureMu.Lock() + changed := t.disableProfiles != r.Features.DisableProfiles || + t.disableNetworks != r.Features.DisableNetworks + t.disableProfiles = r.Features.DisableProfiles + t.disableNetworks = r.Features.DisableNetworks + t.featureMu.Unlock() + // relayoutMenu rebuilds the whole tree, so skip the no-op refresh (common case). + if changed { + t.relayoutMenu() + } +} + +// featuresDisabled returns the cached flags under featureMu. +func (t *Tray) featuresDisabled() (profiles, networks bool) { + t.featureMu.Lock() + defer t.featureMu.Unlock() + return t.disableProfiles, t.disableNetworks +} diff --git a/client/ui/tray_icon.go b/client/ui/tray_icon.go new file mode 100644 index 000000000..70344b2ce --- /dev/null +++ b/client/ui/tray_icon.go @@ -0,0 +1,136 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "runtime" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/ui/services" +) + +func (t *Tray) applyIcon() { + t.statusMu.Lock() + connected := t.connected + statusLabel := t.lastStatus + t.statusMu.Unlock() + hasUpdate := false + if t.updater != nil { + hasUpdate = t.updater.hasUpdate() + } + + log.Infof("tray applyIcon: connected=%v hasUpdate=%v status=%q goos=%s", + connected, hasUpdate, statusLabel, runtime.GOOS) + + icon, dark := t.iconForState() + if runtime.GOOS == "darwin" { + t.tray.SetTemplateIcon(icon) + return + } + if runtime.GOOS == "linux" { + // Wails' Linux SNI backend ignores SetDarkModeIcon (last write wins + // over SetIcon), so iconForState already picked the silhouette by + // panel theme; push that single icon. + t.tray.SetIcon(icon) + return + } + t.tray.SetIcon(icon) + if dark != nil { + t.tray.SetDarkModeIcon(dark) + } +} + +// panelIsDark defaults to true when no detector is wired (panelDark nil — +// non-Linux or portal unavailable), matching the common dark Linux panel. +func (t *Tray) panelIsDark() bool { + if t.panelDark == nil { + return true + } + return t.panelDark() +} + +func (t *Tray) iconForState() (icon, dark []byte) { + t.statusMu.Lock() + connected := t.connected + statusLabel := t.lastStatus + t.statusMu.Unlock() + hasUpdate := false + if t.updater != nil { + hasUpdate = t.updater.hasUpdate() + } + + connecting := strings.EqualFold(statusLabel, services.StatusConnecting) + errored := strings.EqualFold(statusLabel, statusError) || + strings.EqualFold(statusLabel, services.StatusDaemonUnavailable) + needsLogin := strings.EqualFold(statusLabel, services.StatusNeedsLogin) || + strings.EqualFold(statusLabel, services.StatusSessionExpired) || + strings.EqualFold(statusLabel, services.StatusLoginFailed) + + if runtime.GOOS == "darwin" { + switch { + case connecting: + return iconConnectingMacOS, nil + case errored: + return iconErrorMacOS, nil + case needsLogin: + return iconNeedsLoginMacOS, nil + case connected && hasUpdate: + return iconUpdateConnectedMacOS, nil + case connected: + return iconConnectedMacOS, nil + case hasUpdate: + return iconUpdateDisconnectedMacOS, nil + default: + return iconDisconnectedMacOS, nil + } + } + + if runtime.GOOS == "linux" { + // Theme resolved here (black for light panel, white for dark) since + // the SNI backend can't switch per theme (see applyIcon); second + // return is unused on Linux. + dark := t.panelIsDark() + pick := func(black, white []byte) ([]byte, []byte) { + if dark { + return white, nil + } + return black, nil + } + switch { + case connecting: + return pick(iconConnectingMono, iconConnectingMonoDark) + case errored: + return pick(iconErrorMono, iconErrorMonoDark) + case needsLogin: + return pick(iconNeedsLoginMono, iconNeedsLoginMonoDark) + case connected && hasUpdate: + return pick(iconUpdateConnectedMono, iconUpdateConnectedMonoDark) + case connected: + return pick(iconConnectedMono, iconConnectedMonoDark) + case hasUpdate: + return pick(iconUpdateDisconnectedMono, iconUpdateDisconnectedMonoDark) + default: + return pick(iconDisconnectedMono, iconDisconnectedMonoDark) + } + } + + // Windows: colored PNGs. + switch { + case connecting: + return iconConnecting, nil + case errored: + return iconError, nil + case needsLogin: + return iconNeedsLogin, nil + case connected && hasUpdate: + return iconUpdateConnected, nil + case connected: + return iconConnected, iconConnectedDark + case hasUpdate: + return iconUpdateDisconnected, nil + default: + return iconDisconnected, nil + } +} diff --git a/client/ui/tray_label_other.go b/client/ui/tray_label_other.go new file mode 100644 index 000000000..e104bc8e8 --- /dev/null +++ b/client/ui/tray_label_other.go @@ -0,0 +1,7 @@ +//go:build !windows && !android && !ios && !freebsd && !js + +package main + +// menuLabel is the identity on macOS/Linux, which render "&" literally; +// Windows escapes it separately (tray_label_windows.go) to dodge the Win32 mnemonic. +func menuLabel(s string) string { return s } diff --git a/client/ui/tray_label_windows.go b/client/ui/tray_label_windows.go new file mode 100644 index 000000000..8c27ae9ea --- /dev/null +++ b/client/ui/tray_label_windows.go @@ -0,0 +1,11 @@ +//go:build windows + +package main + +import "strings" + +// menuLabel doubles ampersands so Win32 draws a literal "&" instead of +// consuming it as the menu mnemonic prefix (Wails passes the label unescaped). +func menuLabel(s string) string { + return strings.ReplaceAll(s, "&", "&&") +} diff --git a/client/ui/tray_linux.go b/client/ui/tray_linux.go new file mode 100644 index 000000000..1d6e0a48b --- /dev/null +++ b/client/ui/tray_linux.go @@ -0,0 +1,71 @@ +//go:build linux && !386 + +package main + +import ( + "os" + "strings" +) + +// init runs before Wails' own init(), so the env vars are set in time. +func init() { + disableDMABUFRenderer() + disableCompositingMode() + disableWebKitSandboxIfNeeded() +} + +func disableDMABUFRenderer() { + if os.Getenv("WEBKIT_DISABLE_DMABUF_RENDERER") != "" { + return + } + + // WebKitGTK's DMA-BUF renderer leaves a blank-white window on many setups + // (VMs, containers, minimal WMs). Wails only disables it for NVIDIA+Wayland, + // but the issue is broader; software rendering is fine for a small UI. + _ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1") +} + +func disableCompositingMode() { + if os.Getenv("WEBKIT_DISABLE_COMPOSITING_MODE") != "" { + return + } + // Disabling the DMA-BUF renderer alone isn't enough on some Intel setups: the + // GL compositor still hits Mesa's unimplemented DRM-format-modifier paths and + // SIGSEGVs inside g_application_run before the first frame. + _ = os.Setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1") +} + +// disableWebKitSandboxIfNeeded works around WebKitGTK crashing at startup when +// its bwrap sandbox can't create an unprivileged user namespace (containers/VMs, +// or Ubuntu 24.04+ AppArmor restrictions). +func disableWebKitSandboxIfNeeded() { + if _, set := os.LookupEnv("WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS"); set { + return + } + if unprivilegedUsernsAllowed() { + return + } + _ = os.Setenv("WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS", "1") +} + +// unprivilegedUsernsAllowed reports whether the kernel permits unprivileged +// user namespaces (needed by WebKit's bwrap sandbox). Absent knobs are treated +// as allowed, to avoid needlessly weakening the sandbox. +func unprivilegedUsernsAllowed() bool { + // Debian/Ubuntu legacy switch: 0 disables unprivileged user namespaces. + if v, err := os.ReadFile("/proc/sys/kernel/unprivileged_userns_clone"); err == nil { + if strings.TrimSpace(string(v)) == "0" { + return false + } + } + // Ubuntu 24.04+ AppArmor restriction: non-zero restricts/blocks them. + if v, err := os.ReadFile("/proc/sys/kernel/apparmor_restrict_unprivileged_userns"); err == nil { + if strings.TrimSpace(string(v)) != "0" { + return false + } + } + return true +} + +// Linux's tray provider needs the menu recreated rather than updated in place; +// tray.go's rebuildExitNodeMenu already does this, so no extra workaround here. diff --git a/client/ui/tray_notify.go b/client/ui/tray_notify.go new file mode 100644 index 000000000..6c60e3d4b --- /dev/null +++ b/client/ui/tray_notify.go @@ -0,0 +1,58 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "context" + + log "github.com/sirupsen/logrus" + "github.com/wailsapp/wails/v3/pkg/services/notifications" + + "github.com/netbirdio/netbird/client/ui/services" +) + +const notifyIDDaemonOutdated = "netbird-daemon-outdated" + +// sendFn fits both NotificationService.SendNotification and SendNotificationWithActions. +type sendFn func(notifications.NotificationOptions) error + +// safeSendNotification sends a best-effort OS notification, swallowing errors and panics. +// +// The panic guard is load-bearing on Linux: when Wails' notifier fails to +// connect the session bus at startup (headless, unreachable +// DBUS_SESSION_BUS_ADDRESS) it stays registered with a nil *dbus.Conn, so the +// next send nil-derefs inside godbus. Because sends run on a Wails +// event-dispatch goroutine that panic is fatal process-wide; recover() turns +// it into a logged no-op. +func safeSendNotification(send sendFn, what string, opts notifications.NotificationOptions) (err error) { + defer func() { + if r := recover(); r != nil { + log.Errorf("notify %s: recovered from panic (notification bus unavailable): %v", what, r) + err = nil + } + }() + if err := send(opts); err != nil { + log.Errorf("notify %s: %v", what, err) + return err + } + return nil +} + +// notifyIfDaemonOutdated probes the daemon once and fires an OS toast when it +// is reachable but too old for this UI. A probe error means the daemon isn't +// reachable (not outdated), so it is left to the normal connection flow. +func notifyIfDaemonOutdated(compat *services.Compat, notifier *notifications.NotificationService, loc *Localizer) { + ready, err := compat.DaemonReady(context.Background()) + if err != nil { + log.Debugf("daemon compatibility probe: %v", err) + return + } + if ready { + return + } + _ = safeSendNotification(notifier.SendNotification, "daemon-outdated", notifications.NotificationOptions{ + ID: notifyIDDaemonOutdated, + Title: loc.T("notify.daemonOutdated.title"), + Body: loc.T("notify.daemonOutdated.body"), + }) +} diff --git a/client/ui/tray_profiles.go b/client/ui/tray_profiles.go new file mode 100644 index 000000000..5251c138e --- /dev/null +++ b/client/ui/tray_profiles.go @@ -0,0 +1,194 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "context" + "fmt" + "sort" + + log "github.com/sirupsen/logrus" + "github.com/wailsapp/wails/v3/pkg/application" + + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/ui/services" +) + +// formatProfileLabel returns the display label for a profile. Profiles can +// share the same Name, so when more than one profile in profiles carries this +// Name, a short form of the ID is appended to disambiguate the entries. +func formatProfileLabel(profile services.Profile, profiles []services.Profile) string { + count := 0 + for _, p := range profiles { + if p.Name == profile.Name { + count++ + } + } + if count <= 1 { + return profile.Name + } + return fmt.Sprintf("%s (%s)", profile.Name, profilemanager.ID(profile.ID).ShortID()) +} + +// loadConfig caches the active-profile identity and the notifications gate. +// Runs in a startup goroutine so a slow daemon does not block menu construction. +func (t *Tray) loadConfig() { + ctx := context.Background() + + active, err := t.svc.Profiles.GetActive(ctx) + if err != nil { + log.Debugf("get active profile: %v", err) + return + } + // Address the active profile by ID (the daemon resolves it as a handle), + // since display names can collide. ConfigParams no longer matches + // ActiveProfile's shape for a struct conversion now that it carries an ID. + cfg, err := t.svc.Settings.GetConfig(ctx, services.ConfigParams{ + ProfileName: active.ID, + Username: active.Username, + }) + if err != nil { + log.Debugf("get config: %v", err) + return + } + + t.profileMu.Lock() + t.activeProfile = active.ProfileName + t.activeUsername = active.Username + t.notificationsEnabled = !cfg.DisableNotifications + t.profileMu.Unlock() +} + +// loadProfiles fetches the profile list and relayouts the menu. Also called +// from applyStatus to catch flips from another channel (CLI, autoconnect), +// since the daemon emits no active-profile event. Full relayout (not +// Clear()+Add()) is required for KDE/Plasma — see relayoutMenu's doc comment. +func (t *Tray) loadProfiles() { + t.profileLoadMu.Lock() + defer t.profileLoadMu.Unlock() + ctx := context.Background() + + username, err := t.svc.Profiles.Username() + if err != nil { + log.Debugf("get current user: %v", err) + return + } + profiles, err := t.svc.Profiles.List(ctx, username) + if err != nil { + log.Debugf("list profiles: %v", err) + return + } + + t.profilesMu.Lock() + t.profiles = profiles + t.profilesUser = username + t.profilesMu.Unlock() + + t.relayoutMenu() +} + +// fillProfileSubmenu paints cached profile rows into the freshly built submenu. +// Pure UI: never fetches, never calls SetMenu (relayoutMenu owns the SetMenu). +func (t *Tray) fillProfileSubmenu() { + if t.profileSubmenu == nil { + return + } + t.profilesMu.Lock() + profiles := append([]services.Profile(nil), t.profiles...) + username := t.profilesUser + t.profilesMu.Unlock() + + sort.Slice(profiles, func(i, j int) bool { + if profiles[i].Name != profiles[j].Name { + return profiles[i].Name < profiles[j].Name + } + return profiles[i].ID < profiles[j].ID + }) + + // Wails' systray does not reliably propagate a disabled parent to its + // children on every platform, so disable each row explicitly. + disableProfiles, _ := t.featuresDisabled() + + t.profileSubmenu.Clear() + var activeName, activeEmail string + for _, p := range profiles { + id := p.ID + // Display names can collide, so disambiguate with a short ID suffix. + display := formatProfileLabel(p, profiles) + active := p.IsActive + // Add, not AddCheckbox: Wails auto-toggles a checkbox on click before + // OnClick fires, so both old and new would briefly show checked during + // the switch. A plain item with a "✓ " prefix avoids the race. + label := display + if active { + label = "✓ " + display + } + item := t.profileSubmenu.Add(label) + item.OnClick(func(*application.Context) { + log.Infof("tray profile click: profile=%q id=%q wasActive=%v", display, id, active) + if active { + return + } + t.switchProfile(id, display) + }) + item.SetEnabled(!disableProfiles) + if active { + activeName = display + activeEmail = p.Email + } + } + t.profileSubmenu.AddSeparator() + manageProfiles := t.profileSubmenu.Add(t.loc.T("tray.menu.manageProfiles")) + manageProfiles.OnClick(func(*application.Context) { + t.svc.WindowManager.OpenSettings("profiles") + }) + manageProfiles.SetEnabled(!disableProfiles) + log.Infof("tray fillProfileSubmenu: %d profile(s) for user %q, active=%q", len(profiles), username, activeName) + if t.profileSubmenuItem != nil && activeName != "" { + t.profileSubmenuItem.SetLabel(activeName) + } + if t.profileEmailItem != nil { + if activeEmail != "" { + t.profileEmailItem.SetLabel(fmt.Sprintf("(%s)", activeEmail)) + t.profileEmailItem.SetHidden(false) + } else { + t.profileEmailItem.SetHidden(true) + } + } +} + +// switchProfile cancels any in-flight switch before starting a new one, so +// rapid clicks converge to the last selected profile. Optimistic paint and +// event suppression live in ProfileSwitcher, shared with the React Status page. +// switchProfile sends handle (the profile's ID) to the daemon, which resolves +// it precisely even when display names collide. display is used only for the +// failure notification. +func (t *Tray) switchProfile(handle, display string) { + t.profileMu.Lock() + if t.switchCancel != nil { + t.switchCancel() + } + ctx, cancel := context.WithCancel(context.Background()) + t.switchCancel = cancel + t.profileMu.Unlock() + + go func() { + username, err := t.svc.Profiles.Username() + if err != nil { + log.Errorf("tray switchProfile: get current user: %v", err) + return + } + if err := t.svc.ProfileSwitcher.SwitchActive(ctx, services.ProfileRef{ + ProfileName: handle, + Username: username, + }); err != nil { + if ctx.Err() != nil { + return + } + log.Errorf("tray switchProfile: %v", err) + t.notifyError(t.loc.T("notify.error.switchProfile", "profile", display)) + return + } + t.loadProfiles() + }() +} diff --git a/client/ui/tray_session.go b/client/ui/tray_session.go new file mode 100644 index 000000000..6b73ddb49 --- /dev/null +++ b/client/ui/tray_session.go @@ -0,0 +1,271 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "context" + "strconv" + "time" + + log "github.com/sirupsen/logrus" + "github.com/wailsapp/wails/v3/pkg/services/notifications" + + nbstatus "github.com/netbirdio/netbird/client/status" + "github.com/netbirdio/netbird/client/ui/authsession" + "github.com/netbirdio/netbird/client/ui/services" +) + +const ( + notifyIDSessionExpired = "netbird-session-expired" + notifyIDSessionWarning = "netbird-session-warning" + + notifyCategorySessionWarning = "netbird-session-warning" + notifyActionExtendNow = "extend-now" + notifyActionDismiss = "dismiss" + + // finalWarningCountdownSeconds must stay in sync by hand with sessionwatch.FinalWarningLead. + finalWarningCountdownSeconds = 120 +) + +// handleSessionExpired notifies and brings the window forward so the frontend's /login route drives renewal. +func (t *Tray) handleSessionExpired() { + t.notify(t.loc.T("notify.sessionExpired.title"), t.loc.T("notify.sessionExpired.body"), notifyIDSessionExpired) + if t.window != nil { + t.window.SetURL("/#/login") + t.window.Show() + t.window.Focus() + } +} + +// applySessionExpiry refreshes the cached SSO deadline and reports whether it changed. +// Cache-only; the caller relayouts when this returns true. +func (t *Tray) applySessionExpiry(deadline *time.Time, connected bool) bool { + var d time.Time + if connected && deadline != nil { + d = *deadline + } + + t.sessionMu.Lock() + changed := !t.sessionExpiresAt.Equal(d) + t.sessionExpiresAt = d + t.sessionMu.Unlock() + + if changed { + switch { + case deadline == nil: + log.Infof("tray applySessionExpiry: deadline= connected=%v → row hidden", connected) + case deadline.IsZero(): + log.Infof("tray applySessionExpiry: deadline= connected=%v → row hidden", connected) + default: + log.Infof("tray applySessionExpiry: deadline=%s (in %s) connected=%v", + deadline.Format(time.RFC3339), time.Until(*deadline), connected) + } + } + return changed +} + +// runSessionExpiryTicker recomputes the "Expires in …" row label every 30s. Runs until process exit. +func (t *Tray) runSessionExpiryTicker() { + tk := time.NewTicker(30 * time.Second) + for range tk.C { + t.refreshSessionExpiresLabel() + } +} + +// refreshSessionExpiresLabel updates only the countdown label, no relayout, to avoid disturbing an open menu. +// The item is snapshotted under menuMu since buildMenu reassigns it on every relayout. +func (t *Tray) refreshSessionExpiresLabel() { + t.menuMu.Lock() + item := t.sessionExpiresItem + t.menuMu.Unlock() + if item == nil { + return + } + t.sessionMu.Lock() + deadline := t.sessionExpiresAt + t.sessionMu.Unlock() + if deadline.IsZero() { + return + } + remaining := t.formatSessionRemaining(time.Until(deadline)) + item.SetLabel(t.loc.T("tray.session.expiresIn", "remaining", remaining)) +} + +// formatSessionRemaining renders d as a localised long-form string picking the largest non-zero unit. +// Singular/plural keys are split per language for proper translation. +func (t *Tray) formatSessionRemaining(d time.Duration) string { + switch { + case d < time.Minute: + return t.loc.T("tray.session.unit.lessThanMinute") + case d < time.Hour: + m := int(d / time.Minute) + if m == 1 { + return t.loc.T("tray.session.unit.minute") + } + return t.loc.T("tray.session.unit.minutes", "count", strconv.Itoa(m)) + case d < 24*time.Hour: + h := int((d + 30*time.Minute) / time.Hour) + if h == 1 { + return t.loc.T("tray.session.unit.hour") + } + return t.loc.T("tray.session.unit.hours", "count", strconv.Itoa(h)) + default: + days := int((d + 12*time.Hour) / (24 * time.Hour)) + if days == 1 { + return t.loc.T("tray.session.unit.day") + } + return t.loc.T("tray.session.unit.days", "count", strconv.Itoa(days)) + } +} + +// registerSessionWarningCategory wires the OS notification category and response handler for the expiry warning. +// Errors are swallowed since the worst case is a plain notification without buttons. +func (t *Tray) registerSessionWarningCategory() { + if t.svc.Notifier == nil { + return + } + if err := t.svc.Notifier.RegisterNotificationCategory(notifications.NotificationCategory{ + ID: notifyCategorySessionWarning, + Actions: []notifications.NotificationAction{ + {ID: notifyActionExtendNow, Title: t.loc.T("notify.sessionWarning.extend")}, + {ID: notifyActionDismiss, Title: t.loc.T("notify.sessionWarning.dismiss")}, + }, + }); err != nil { + log.Debugf("register session-warning notification category: %v", err) + } + t.svc.Notifier.OnNotificationResponse(func(result notifications.NotificationResult) { + if result.Error != nil { + log.Debugf("notification response error: %v", result.Error) + return + } + if result.Response.CategoryID != notifyCategorySessionWarning { + return + } + switch result.Response.ActionIdentifier { + case notifyActionExtendNow, notifications.DefaultActionIdentifier: + // DefaultActionIdentifier is the body-click on platforms with no separate buttons; treat as Extend. + go t.runExtendSession() + case notifyActionDismiss: + go t.dismissSessionWarning() + } + }) +} + +// buildSessionWarningBody composes the localised notification body from the daemon's metadata. +// The daemon has no locale, so it ships an RFC3339 deadline the tray turns into a user-language sentence. +// Falls back to a generic string when metadata is missing or unparsable. +func (t *Tray) buildSessionWarningBody(meta map[string]string) string { + if meta == nil { + return t.loc.T("notify.sessionWarning.bodyGeneric") + } + raw := meta[authsession.MetaExpiresAt] + if raw == "" { + return t.loc.T("notify.sessionWarning.bodyGeneric") + } + deadline, err := authsession.ParseExpiresAt(raw) + if err != nil { + return t.loc.T("notify.sessionWarning.bodyGeneric") + } + remaining := nbstatus.FormatRemainingDuration(time.Until(deadline)) + return t.loc.T("notify.sessionWarning.body", "remaining", remaining) +} + +// notifySessionWarning sends the interactive expiry notification, falling back to plain notify when the +// with-actions variant is unavailable (older platform impls, or a bare Notifier in tests). +func (t *Tray) notifySessionWarning(title, body string) { + if t.svc.Notifier == nil { + return + } + err := safeSendNotification(t.svc.Notifier.SendNotificationWithActions, "session-warning with actions", notifications.NotificationOptions{ + ID: notifyIDSessionWarning, + Title: title, + Body: body, + CategoryID: notifyCategorySessionWarning, + }) + if err != nil { + // A recovered panic returns nil err, so a dead bus correctly skips this fallback (it would panic too). + t.notify(title, body, notifyIDSessionWarning) + } +} + +// runExtendSession drives the daemon's RequestExtend + WaitExtend pair, opening the browser via Connection.OpenURL. +// Errors surface as notifyError rather than foreground UI, since the warning may fire while the window is closed. +func (t *Tray) runExtendSession() { + if t.svc.Session == nil || t.svc.Connection == nil { + log.Debugf("session-warning: extend requested but services not wired") + return + } + ctx := context.Background() + + start, err := t.svc.Session.RequestExtend(ctx, services.ExtendStartParams{}) + if err != nil { + log.Warnf("session-warning: RequestExtend failed: %v", err) + t.notifyError(t.loc.T("notify.sessionWarning.failed")) + return + } + + uri := start.VerificationURIComplete + if uri == "" { + uri = start.VerificationURI + } + if uri != "" { + if err := t.svc.Connection.OpenURL(uri); err != nil { + log.Debugf("session-warning: opening verification URL: %v", err) + } + } + + result, err := t.svc.Session.WaitExtend(ctx, services.ExtendWaitParams{ + DeviceCode: start.DeviceCode, + UserCode: start.UserCode, + }) + if err != nil { + log.Warnf("session-warning: WaitExtend failed: %v", err) + t.notifyError(t.loc.T("notify.sessionWarning.failed")) + return + } + if result.Preempted { + // Another UI surface owns the flow; stay silent so the user only sees the surviving flow's outcome. + log.Debugf("session-warning: WaitExtend preempted by a newer flow") + return + } + t.notify(t.loc.T("notify.sessionWarning.successTitle"), t.loc.T("notify.sessionWarning.successBody"), notifyIDSessionWarning) +} + +// dismissSessionWarning tells the daemon to silence the fallback dialog for the current deadline. +// Best-effort: a failure only means the dialog will still appear. +func (t *Tray) dismissSessionWarning() { + if t.svc.Session == nil { + return + } + if err := t.svc.Session.DismissWarning(context.Background()); err != nil { + log.Debugf("session-warning: DismissWarning failed: %v", err) + } +} + +// openSessionExpiration fires the fallback dialog when the earlier warning notification wasn't dismissed. +// Idempotent on the WindowManager side. +func (t *Tray) openSessionExpiration() { + if t.svc.WindowManager == nil { + return + } + t.svc.WindowManager.OpenSessionExpiration(finalWarningCountdownSeconds) +} + +// openSessionExtendFlow opens the SessionExpiration window seeded with the cached deadline's remaining time, +// for the "Expires in …" tray row. No-ops when the deadline is unknown or elapsed. +func (t *Tray) openSessionExtendFlow() { + if t.svc.WindowManager == nil { + return + } + t.sessionMu.Lock() + deadline := t.sessionExpiresAt + t.sessionMu.Unlock() + if deadline.IsZero() { + return + } + seconds := int(time.Until(deadline).Seconds()) + if seconds <= 0 { + return + } + t.svc.WindowManager.OpenSessionExpiration(seconds) +} diff --git a/client/ui/tray_status.go b/client/ui/tray_status.go new file mode 100644 index 000000000..793f1785a --- /dev/null +++ b/client/ui/tray_status.go @@ -0,0 +1,126 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "strings" + + "github.com/wailsapp/wails/v3/pkg/application" + + "github.com/netbirdio/netbird/client/ui/services" +) + +func (t *Tray) onStatusEvent(ev *application.CustomEvent) { + st, ok := ev.Data.(services.Status) + if !ok { + return + } + t.applyStatus(st) +} + +// applyStatus repaints the tray from a daemon snapshot. Icon refresh is skipped +// when no icon-relevant input changed: the daemon emits rapid SubscribeStatus +// bursts during health probes that would otherwise spam Shell_NotifyIcon. +func (t *Tray) applyStatus(st services.Status) { + t.statusMu.Lock() + connected := strings.EqualFold(st.Status, services.StatusConnected) + iconChanged := connected != t.connected || st.Status != t.lastStatus + // The daemon re-emits SessionExpired on every snapshot while expired; act + // only on the transition into it so the notification fires once. + sessionExpiredEnter := strings.EqualFold(st.Status, services.StatusSessionExpired) && + !strings.EqualFold(t.lastStatus, services.StatusSessionExpired) + + triggerLogin := t.consumePendingConnectLogin(st.Status) + + daemonVersionChanged := st.DaemonVersion != "" && st.DaemonVersion != t.lastDaemonVersion + t.connected = connected + t.lastStatus = st.Status + if daemonVersionChanged { + t.lastDaemonVersion = st.DaemonVersion + } + + revisionChanged := st.NetworksRevision != t.lastNetworksRevision + t.lastNetworksRevision = st.NetworksRevision + t.statusMu.Unlock() + + if triggerLogin { + t.app.Event.Emit(services.EventTriggerLogin) + } + + // Cache-only; the row is painted by the relayout below. + sessionChanged := t.applySessionExpiry(st.SessionExpiresAt, connected) + + if iconChanged { + t.applyIcon() + } + // All repainting goes through relayoutMenu (menuMu-serialised): applyStatus + // runs concurrently with itself and with relayouts, so in-place item + // mutation would race the buildMenu pointer swap. + if iconChanged || daemonVersionChanged || sessionChanged { + t.relayoutMenu() + } + // The revision is the only reliable signal: candidate routes never appear + // in the peer-status snapshot, so a removed exit node would go unnoticed. + if iconChanged || revisionChanged { + go t.refreshExitNodes() + } + // The daemon emits no active-profile event, so profile flips driven + // elsewhere (CLI, autoconnect) surface via status transitions. + if iconChanged { + go t.loadProfiles() + } + if sessionExpiredEnter { + t.handleSessionExpired() + } +} + +// consumePendingConnectLogin acts on the SSO auto-handoff flag armed by +// handleConnect. Returns true on NeedsLogin so the browser-login flow starts +// without a second Connect click; clears the flag on any terminal state so a +// stale flag can't fire on a later daemon flip. Must hold statusMu. +func (t *Tray) consumePendingConnectLogin(status string) bool { + if !t.pendingConnectLogin { + return false + } + switch { + case strings.EqualFold(status, services.StatusNeedsLogin): + t.pendingConnectLogin = false + return true + case strings.EqualFold(status, services.StatusConnected), + strings.EqualFold(status, services.StatusIdle), + strings.EqualFold(status, services.StatusLoginFailed), + strings.EqualFold(status, services.StatusSessionExpired), + strings.EqualFold(status, services.StatusDaemonUnavailable): + t.pendingConnectLogin = false + } + return false +} + +// applyStatusIndicator sets the status dot bitmap. Call only from relayoutMenu +// (menuMu held): on macOS the bitmap repaints via the relayout's trailing +// SetMenu, not here — the tree is half-built. +func (t *Tray) applyStatusIndicator(status string) { + if t.statusItem == nil { + return + } + t.statusItem.SetBitmap(statusIndicatorBitmap(status)) +} + +func statusIndicatorBitmap(status string) []byte { + switch { + case strings.EqualFold(status, services.StatusConnected): + return iconMenuDotConnected + case strings.EqualFold(status, services.StatusConnecting): + return iconMenuDotConnecting + case strings.EqualFold(status, services.StatusNeedsLogin), + strings.EqualFold(status, services.StatusSessionExpired): + return iconMenuDotConnecting + case strings.EqualFold(status, services.StatusLoginFailed), + strings.EqualFold(status, statusError): + return iconMenuDotError + case strings.EqualFold(status, services.StatusDaemonUnavailable): + return iconMenuDotOffline + default: + return iconMenuDotIdle + } +} diff --git a/client/ui/tray_status_enabled_linux.go b/client/ui/tray_status_enabled_linux.go new file mode 100644 index 000000000..ab7f869b8 --- /dev/null +++ b/client/ui/tray_status_enabled_linux.go @@ -0,0 +1,8 @@ +//go:build linux + +package main + +// statusRowEnabled keeps the top status row enabled on Linux: a disabled row +// paints greyed-out, washing out the status dot. The row has no OnClick, so +// enabling only affects drawing. +func statusRowEnabled() bool { return true } diff --git a/client/ui/tray_status_enabled_other.go b/client/ui/tray_status_enabled_other.go new file mode 100644 index 000000000..606a7d702 --- /dev/null +++ b/client/ui/tray_status_enabled_other.go @@ -0,0 +1,7 @@ +//go:build !windows && !linux && !android && !ios && !freebsd && !js + +package main + +// statusRowEnabled is false on macOS: disabling the row dims the label (signalling +// non-clickable) while keeping the bitmap opaque, so the coloured dot stays visible. +func statusRowEnabled() bool { return false } diff --git a/client/ui/tray_status_enabled_windows.go b/client/ui/tray_status_enabled_windows.go new file mode 100644 index 000000000..06750b7c0 --- /dev/null +++ b/client/ui/tray_status_enabled_windows.go @@ -0,0 +1,7 @@ +//go:build windows + +package main + +// statusRowEnabled is always true on Windows: the Win32 disabled-state mask +// desaturates the row's HBITMAP, which would grey out the coloured status dot. +func statusRowEnabled() bool { return true } diff --git a/client/ui/tray_theme_linux.go b/client/ui/tray_theme_linux.go new file mode 100644 index 000000000..a3249e57a --- /dev/null +++ b/client/ui/tray_theme_linux.go @@ -0,0 +1,134 @@ +//go:build linux && !(linux && 386) + +package main + +// Wails v3's Linux SNI backend ignores SetDarkModeIcon (it just calls setIcon, +// last write wins) and SNI carries no panel dark/light hint, so we detect the +// desktop colour scheme ourselves and pick the silhouette in iconForState. +// The live watcher is in tray_theme_watcher_linux.go. + +import ( + "bufio" + "os" + "path/filepath" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +// startTrayTheme seeds t.panelDark and repaints on colour-scheme flips. Must +// run before the first applyIcon so the initial paint uses the right silhouette. +func (t *Tray) startTrayTheme() { + w := startThemeWatcher(func() { t.applyIcon() }) + t.panelDark = w.IsDark +} + +// isKDE reports whether the current desktop is KDE Plasma. XDG_CURRENT_DESKTOP +// is a colon-separated list (e.g. "ubuntu:KDE"), so match per token. +func isKDE() bool { + for _, d := range strings.Split(os.Getenv("XDG_CURRENT_DESKTOP"), ":") { + if strings.EqualFold(strings.TrimSpace(d), "KDE") { + return true + } + } + return false +} + +// kdeglobalsPath returns the user kdeglobals path. We read only this file, not +// the full XDG_CONFIG_DIRS cascade: Plasma writes the active scheme here, and a +// missing Complementary group falls back to the portal. +func kdeglobalsPath() string { + if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" { + return filepath.Join(dir, "kdeglobals") + } + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, ".config", "kdeglobals") +} + +// kdePanelIsDark reports whether the KDE Plasma panel is dark by the luma of +// its "Complementary" background (the colour Plasma paints the tray with). ok +// is false when this isn't KDE or the colour can't be read, so the caller falls +// through to the portal/GTK path. +func kdePanelIsDark() (dark, ok bool) { + if !isKDE() { + return false, false + } + path := kdeglobalsPath() + if path == "" { + return false, false + } + rgb, ok := readKdeComplementaryBackground(path) + if !ok { + return false, false + } + return isDarkRGB(rgb[0], rgb[1], rgb[2]), true +} + +// readKdeComplementaryBackground parses kdeglobals for +// [Colors:Complementary] BackgroundNormal and returns its R,G,B (0-255). +func readKdeComplementaryBackground(path string) (rgb [3]uint8, ok bool) { + f, err := os.Open(path) + if err != nil { + log.Debugf("tray theme: kdeglobals open failed, using portal: %v", err) + return rgb, false + } + defer func() { _ = f.Close() }() + + const group = "[Colors:Complementary]" + inGroup := false + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "[") { + inGroup = line == group + continue + } + if !inGroup { + continue + } + key, val, found := strings.Cut(line, "=") + if !found || strings.TrimSpace(key) != "BackgroundNormal" { + continue + } + return parseRGB(strings.TrimSpace(val)) + } + return rgb, false +} + +// parseRGB parses KDE's "r,g,b" colour triple into bytes. +func parseRGB(s string) (rgb [3]uint8, ok bool) { + parts := strings.Split(s, ",") + if len(parts) != 3 { + return rgb, false + } + for i, p := range parts { + n, err := strconv.Atoi(strings.TrimSpace(p)) + if err != nil || n < 0 || n > 255 { + return rgb, false + } + rgb[i] = uint8(n) + } + return rgb, true +} + +// isDarkRGB reports whether a colour is dark via Rec. 601 luma, split at the +// 128 midpoint. +func isDarkRGB(r, g, b uint8) bool { + luma := (299*int(r) + 587*int(g) + 114*int(b)) / 1000 + return luma < 128 +} + +// gtkThemeIsDark inspects the GTK_THEME env var. Empty (no override) is treated +// as dark to match the default-dark fallback used elsewhere. +func gtkThemeIsDark() bool { + theme := os.Getenv("GTK_THEME") + if theme == "" { + return true + } + // GTK_THEME is "Name[:variant]"; the dark variant is ":dark". + return strings.Contains(strings.ToLower(theme), ":dark") +} diff --git a/client/ui/tray_theme_linux_test.go b/client/ui/tray_theme_linux_test.go new file mode 100644 index 000000000..f14f08d7f --- /dev/null +++ b/client/ui/tray_theme_linux_test.go @@ -0,0 +1,86 @@ +//go:build linux && !(linux && 386) + +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestReadKdeComplementaryBackground(t *testing.T) { + // Mirrors the KDE test VM's kdeglobals: Window light, Complementary dark. + // The tray sits on the panel, which Plasma paints from Complementary, so + // the panel is dark even though the global color-scheme is Light. + content := `[Colors:Window] +BackgroundNormal=239,240,241 + +[Colors:Complementary] +BackgroundAlternate=27,30,32 +BackgroundNormal=42,46,50 + +[General] +ColorSchemeHash=0be804dba87e3512aeb4be3d78ed981f59f0f2f4 +` + path := filepath.Join(t.TempDir(), "kdeglobals") + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + + rgb, ok := readKdeComplementaryBackground(path) + if !ok { + t.Fatal("expected to find Complementary BackgroundNormal") + } + if rgb != [3]uint8{42, 46, 50} { + t.Fatalf("rgb = %v, want [42 46 50]", rgb) + } + if !isDarkRGB(rgb[0], rgb[1], rgb[2]) { + t.Fatal("panel colour 42,46,50 should be dark") + } + // The Window background (what color-scheme reflects) is light — the bug + // this fix addresses is picking the icon from that instead of the panel. + if isDarkRGB(239, 240, 241) { + t.Fatal("window colour 239,240,241 should be light") + } +} + +func TestReadKdeComplementaryBackgroundMissingGroup(t *testing.T) { + path := filepath.Join(t.TempDir(), "kdeglobals") + if err := os.WriteFile(path, []byte("[Colors:Window]\nBackgroundNormal=1,2,3\n"), 0o600); err != nil { + t.Fatal(err) + } + if _, ok := readKdeComplementaryBackground(path); ok { + t.Fatal("expected not-ok when Complementary group is absent") + } +} + +func TestParseRGB(t *testing.T) { + if _, ok := parseRGB("1,2"); ok { + t.Fatal("two components should fail") + } + if _, ok := parseRGB("300,0,0"); ok { + t.Fatal("out-of-range should fail") + } + if _, ok := parseRGB("a,b,c"); ok { + t.Fatal("non-numeric should fail") + } + rgb, ok := parseRGB(" 10 , 20 , 30 ") + if !ok || rgb != [3]uint8{10, 20, 30} { + t.Fatalf("parseRGB = %v ok=%v, want [10 20 30] true", rgb, ok) + } +} + +func TestIsDarkRGB(t *testing.T) { + if !isDarkRGB(0, 0, 0) { + t.Fatal("black is dark") + } + if isDarkRGB(255, 255, 255) { + t.Fatal("white is light") + } + if !isDarkRGB(42, 46, 50) { + t.Fatal("Breeze panel grey is dark") + } + if isDarkRGB(239, 240, 241) { + t.Fatal("Breeze window grey is light") + } +} diff --git a/client/ui/tray_theme_other.go b/client/ui/tray_theme_other.go new file mode 100644 index 000000000..3f85e1603 --- /dev/null +++ b/client/ui/tray_theme_other.go @@ -0,0 +1,7 @@ +//go:build (!linux || (linux && 386)) && !android && !ios && !freebsd && !js + +package main + +func (t *Tray) startTrayTheme() { + // No-op off Linux: leaves panelDark nil so panelIsDark uses its default. +} diff --git a/client/ui/tray_theme_watcher_linux.go b/client/ui/tray_theme_watcher_linux.go new file mode 100644 index 000000000..b9bafe30b --- /dev/null +++ b/client/ui/tray_theme_watcher_linux.go @@ -0,0 +1,246 @@ +//go:build linux && !(linux && 386) + +package main + +// Sources: the freedesktop Settings portal's SettingChanged signal, and on KDE +// the kdeglobals file (the portal's color-scheme doesn't track the panel's +// Complementary colour — see readDarkMode). The dark/light decision lives in +// tray_theme_linux.go; this file owns the session-bus connection and subscriptions. + +import ( + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/godbus/dbus/v5" + log "github.com/sirupsen/logrus" +) + +const ( + portalBusName = "org.freedesktop.portal.Desktop" + portalObjectPath = "/org/freedesktop/portal/desktop" + portalSettings = "org.freedesktop.portal.Settings" + + appearanceNamespace = "org.freedesktop.appearance" + colorSchemeKey = "color-scheme" + + colorSchemeNoPreference = 0 + colorSchemePreferDark = 1 + colorSchemePreferLight = 2 +) + +// themeWatcher owns a private session-bus connection so its signal subscription +// is isolated from the SNI watcher's. +type themeWatcher struct { + conn *dbus.Conn + onChange func() + + mu sync.Mutex + darkMode bool +} + +// startThemeWatcher returns nil if the session bus is unavailable; callers treat +// a nil watcher as "no preference", keeping the default-dark icon. +func startThemeWatcher(onChange func()) *themeWatcher { + conn, err := dbus.SessionBusPrivate() + if err != nil { + log.Debugf("tray theme: session bus unavailable, defaulting to dark icons: %v", err) + return nil + } + if err := conn.Auth(nil); err != nil { + _ = conn.Close() + log.Debugf("tray theme: dbus auth failed: %v", err) + return nil + } + if err := conn.Hello(); err != nil { + _ = conn.Close() + log.Debugf("tray theme: dbus hello failed: %v", err) + return nil + } + + w := &themeWatcher{conn: conn, onChange: onChange} + w.darkMode = w.readDarkMode() + + if err := w.subscribe(); err != nil { + log.Debugf("tray theme: SettingChanged subscription failed, theme is static: %v", err) + // Keep the connection: the seeded darkMode value is still useful. + } + + // The portal's signal doesn't track KDE's panel Complementary colour. + if isKDE() { + w.watchKdeglobals() + } + + log.Infof("tray theme: panel dark mode = %v", w.IsDark()) + return w +} + +// IsDark reports true for a nil watcher, so the icon defaults to the white +// silhouette suiting the common dark Linux panel. +func (w *themeWatcher) IsDark() bool { + if w == nil { + return true + } + w.mu.Lock() + defer w.mu.Unlock() + return w.darkMode +} + +// readDarkMode resolves whether the panel the tray icon sits on is dark. +// +// On KDE the freedesktop color-scheme is the application preference, not the +// panel's: Plasma paints its panel from the Breeze "Complementary" group, which +// stays dark even under a Light global scheme, so we read the panel background +// from kdeglobals first and decide by its luma. Off KDE the color-scheme portal +// is the source; on "no preference" (0) or when unavailable we fall back to +// GTK_THEME (":dark" suffix ⇒ dark), then default to dark. +func (w *themeWatcher) readDarkMode() bool { + if dark, ok := kdePanelIsDark(); ok { + return dark + } + switch w.readColorScheme() { + case colorSchemePreferDark: + return true + case colorSchemePreferLight: + return false + default: + return gtkThemeIsDark() + } +} + +// readColorScheme returns the raw freedesktop color-scheme value, or +// colorSchemeNoPreference when the portal can't be reached. +func (w *themeWatcher) readColorScheme() uint32 { + obj := w.conn.Object(portalBusName, portalObjectPath) + call := obj.Call(portalSettings+".Read", 0, appearanceNamespace, colorSchemeKey) + if call.Err != nil { + log.Debugf("tray theme: portal Read failed, falling back to GTK_THEME: %v", call.Err) + return colorSchemeNoPreference + } + + var v dbus.Variant + if err := call.Store(&v); err != nil { + log.Debugf("tray theme: portal Read decode failed, falling back to GTK_THEME: %v", err) + return colorSchemeNoPreference + } + + return variantToColorScheme(v) +} + +func (w *themeWatcher) subscribe() error { + if err := w.conn.AddMatchSignal( + dbus.WithMatchObjectPath(portalObjectPath), + dbus.WithMatchInterface(portalSettings), + dbus.WithMatchMember("SettingChanged"), + ); err != nil { + return err + } + + sigs := make(chan *dbus.Signal, 8) + w.conn.Signal(sigs) + go w.loop(sigs) + return nil +} + +func (w *themeWatcher) loop(sigs chan *dbus.Signal) { + for sig := range sigs { + if sig.Name != portalSettings+".SettingChanged" { + continue + } + // Signal body: (namespace string, key string, value variant). + if len(sig.Body) < 3 { + continue + } + namespace, _ := sig.Body[0].(string) + key, _ := sig.Body[1].(string) + if namespace != appearanceNamespace || key != colorSchemeKey { + continue + } + if _, ok := sig.Body[2].(dbus.Variant); !ok { + continue + } + + // Re-resolve via readDarkMode, not the signal value: under KDE the panel + // colour comes from kdeglobals, so the signal value would be wrong. + w.update() + } +} + +func (w *themeWatcher) update() { + dark := w.readDarkMode() + w.mu.Lock() + changed := dark != w.darkMode + w.darkMode = dark + w.mu.Unlock() + + if changed && w.onChange != nil { + log.Infof("tray theme: panel dark mode changed to %v", dark) + w.onChange() + } +} + +// watchKdeglobals watches the parent directory, not the file: KDE rewrites +// kdeglobals atomically (write-temp + rename), which would drop an inotify watch +// on the original inode. Filtering by name re-arms implicitly. +func (w *themeWatcher) watchKdeglobals() { + path := kdeglobalsPath() + if path == "" { + return + } + dir, name := filepath.Split(path) + + fw, err := fsnotify.NewWatcher() + if err != nil { + log.Debugf("tray theme: kdeglobals watcher unavailable, theme is static: %v", err) + return + } + if err := fw.Add(filepath.Clean(dir)); err != nil { + log.Debugf("tray theme: watching %s failed, theme is static: %v", dir, err) + _ = fw.Close() + return + } + + go func() { + defer func() { _ = fw.Close() }() + for { + select { + case event, ok := <-fw.Events: + if !ok { + return + } + if filepath.Base(event.Name) != name { + continue + } + if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 { + continue + } + w.update() + case err, ok := <-fw.Errors: + if !ok { + return + } + log.Debugf("tray theme: kdeglobals watch error: %v", err) + } + } + }() +} + +// variantToColorScheme unwraps the color-scheme variant; the portal nests it one level. +func variantToColorScheme(v dbus.Variant) uint32 { + inner := v.Value() + if nested, ok := inner.(dbus.Variant); ok { + inner = nested.Value() + } + + switch n := inner.(type) { + case uint32: + return n + case int32: + return uint32(n) + case uint8: + return uint32(n) + default: + log.Debugf("tray theme: unexpected color-scheme type %T, assuming no preference", inner) + return colorSchemeNoPreference + } +} diff --git a/client/ui/tray_update.go b/client/ui/tray_update.go new file mode 100644 index 000000000..2d79cff05 --- /dev/null +++ b/client/ui/tray_update.go @@ -0,0 +1,197 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "context" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/services/notifications" + + "github.com/netbirdio/netbird/client/ui/services" + "github.com/netbirdio/netbird/client/ui/updater" +) + +// trayUpdater owns the tray UI that reacts to auto-update. Composed inside Tray. +type trayUpdater struct { + app *application.App + window *application.WebviewWindow + update *services.Update + notifier *notifications.NotificationService + loc *Localizer + onIconChange func() + // onMenuChange drives a full tray relayout: the update row lives in the + // About submenu, which KDE/Plasma caches on first open and never re-fetches + // on a plain SetLabel/SetHidden — only a relayout (fresh submenu ids) repaints. + onMenuChange func() + + mu sync.Mutex + item *application.MenuItem + state updater.State + notifiedVersion string + progressWindowOpen bool +} + +func newTrayUpdater(app *application.App, window *application.WebviewWindow, update *services.Update, notifier *notifications.NotificationService, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater { + u := &trayUpdater{ + app: app, + window: window, + update: update, + notifier: notifier, + loc: loc, + onIconChange: onIconChange, + onMenuChange: onMenuChange, + } + app.Event.On(updater.EventStateChanged, u.onStateEvent) + // Seed from cached state to cover an event that fired before wiring completed. + u.state = update.GetState() + return u +} + +// attach (re)binds the menu item on each Tray.buildMenu run. The caller owns the +// item's OnClick handler. +func (u *trayUpdater) attach(item *application.MenuItem) { + u.mu.Lock() + u.item = item + state := u.state + u.mu.Unlock() + u.refreshMenuItem(state) +} + +// hasUpdate reports whether the tray should paint the "update available" icon. +func (u *trayUpdater) hasUpdate() bool { + u.mu.Lock() + defer u.mu.Unlock() + return u.state.Available +} + +// applyLanguage re-renders the menu item label after a locale switch. +func (u *trayUpdater) applyLanguage() { + u.mu.Lock() + state := u.state + u.mu.Unlock() + u.refreshMenuItem(state) +} + +// handleClick opens the GitHub releases page when not Enforced, otherwise shows +// the progress page and asks the daemon to start the installer. +func (u *trayUpdater) handleClick() { + u.mu.Lock() + state := u.state + u.mu.Unlock() + + if !state.Enforced { + _ = u.app.Browser.OpenURL(urlGitHubReleases) + return + } + + u.openProgressWindow(state.Version) + + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if _, err := u.update.Trigger(ctx); err != nil { + log.Errorf("trigger update: %v", err) + } + }() +} + +func (u *trayUpdater) onStateEvent(ev *application.CustomEvent) { + st, ok := ev.Data.(updater.State) + if !ok { + log.Warnf("update state event payload not UpdateState: %T", ev.Data) + return + } + u.applyState(st) +} + +// applyState diffs st against the cached state and drives the resulting side +// effects: icon repaint, menu refresh, new-version notification, progress window. +func (u *trayUpdater) applyState(st updater.State) { + u.mu.Lock() + prev := u.state + u.state = st + + sendNotify := st.Available && st.Version != "" && st.Version != u.notifiedVersion + if sendNotify { + u.notifiedVersion = st.Version + } + + showWindow := st.Installing && !u.progressWindowOpen + if showWindow { + u.progressWindowOpen = true + } else if !st.Installing { + u.progressWindowOpen = false + } + u.mu.Unlock() + + // Full relayout rather than in-place: KDE layout-caches the About submenu, so + // a direct SetLabel/SetHidden wouldn't paint. Fall back if no hook was wired. + if u.onMenuChange != nil { + u.onMenuChange() + } else { + u.refreshMenuItem(st) + } + if prev.Available != st.Available && u.onIconChange != nil { + u.onIconChange() + } + if sendNotify { + u.sendUpdateNotification(st) + } + if showWindow { + u.openProgressWindow(st.Version) + } +} + +func (u *trayUpdater) refreshMenuItem(st updater.State) { + u.mu.Lock() + item := u.item + u.mu.Unlock() + if item == nil { + return + } + + if !st.Available { + item.SetHidden(true) + return + } + if st.Enforced { + item.SetLabel(u.loc.T("tray.menu.installVersion", "version", st.Version)) + } else { + item.SetLabel(u.loc.T("tray.menu.downloadLatest")) + } + item.SetHidden(false) +} + +func (u *trayUpdater) sendUpdateNotification(st updater.State) { + if u.notifier == nil { + return + } + body := u.loc.T("notify.update.body", "version", st.Version) + if st.Enforced { + body += u.loc.T("notify.update.enforcedSuffix") + } + _ = safeSendNotification(u.notifier.SendNotification, "update", notifications.NotificationOptions{ + ID: notifyIDUpdatePrefix + st.Version, + Title: u.loc.T("notify.update.title"), + Body: body, + }) +} + +// openProgressWindow points the main window at the /update progress page and +// brings it forward. +func (u *trayUpdater) openProgressWindow(version string) { + if u.window == nil { + return + } + url := "/#/update" + if version != "" { + url += "?version=" + version + } + u.window.SetURL(url) + u.window.Show() + u.window.Focus() +} diff --git a/client/ui/tray_watcher_linux.go b/client/ui/tray_watcher_linux.go new file mode 100644 index 000000000..38476179a --- /dev/null +++ b/client/ui/tray_watcher_linux.go @@ -0,0 +1,176 @@ +//go:build linux && !(linux && 386) + +package main + +// In-process org.kde.StatusNotifierWatcher for minimal WMs (Fluxbox, OpenBox, +// i3) that ship no watcher. When an XEmbed tray exists (_NET_SYSTEM_TRAY_S0), +// an in-process XEmbed host bridges the SNI icon into it. + +import ( + "sync" + "time" + + "github.com/godbus/dbus/v5" + log "github.com/sirupsen/logrus" +) + +const ( + watcherName = "org.kde.StatusNotifierWatcher" + watcherPath = "/StatusNotifierWatcher" + watcherIface = "org.kde.StatusNotifierWatcher" + + // The UI is often autostarted before the panel on minimal WMs, so a single + // startup probe would miss a tray that appears a second later. + watcherProbeInterval = 500 * time.Millisecond + watcherProbeTimeout = 10 * time.Second +) + +type statusNotifierWatcher struct { + conn *dbus.Conn + items []string + hosts map[string]*xembedHost + hostsMu sync.Mutex +} + +// RegisterStatusNotifierItem is the D-Bus method called by tray clients. +// sender is injected by godbus and is not part of the D-Bus signature. +func (w *statusNotifierWatcher) RegisterStatusNotifierItem(sender dbus.Sender, service string) *dbus.Error { + for _, s := range w.items { + if s == service { + return nil + } + } + w.items = append(w.items, service) + log.Debugf("StatusNotifierWatcher: registered item %q from %s", service, sender) + + go w.tryStartXembedHost(string(sender), dbus.ObjectPath(service)) + return nil +} + +// RegisterStatusNotifierHost is required by the protocol but unused here. +func (w *statusNotifierWatcher) RegisterStatusNotifierHost(service string) *dbus.Error { + log.Debugf("StatusNotifierWatcher: host registered %q", service) + return nil +} + +// tryStartXembedHost is a no-op when no XEmbed tray manager is available. +func (w *statusNotifierWatcher) tryStartXembedHost(busName string, objPath dbus.ObjectPath) { + w.hostsMu.Lock() + defer w.hostsMu.Unlock() + + if _, exists := w.hosts[busName]; exists { + return + } + + // Private session bus so our signal subscriptions don't reach Wails' + // signal handler, which panics on unexpected signals. + sessionConn, err := dbus.SessionBusPrivate() + if err != nil { + log.Debugf("StatusNotifierWatcher: cannot open private session bus for XEmbed host: %v", err) + return + } + if err := sessionConn.Auth(nil); err != nil { + log.Debugf("StatusNotifierWatcher: XEmbed host auth failed: %v", err) + closeBus(sessionConn) + return + } + if err := sessionConn.Hello(); err != nil { + log.Debugf("StatusNotifierWatcher: XEmbed host Hello failed: %v", err) + closeBus(sessionConn) + return + } + + host, err := newXembedHost(sessionConn, busName, objPath) + if err != nil { + log.Debugf("StatusNotifierWatcher: XEmbed host not started: %v", err) + closeBus(sessionConn) + return + } + + w.hosts[busName] = host + go host.run() + log.Infof("StatusNotifierWatcher: XEmbed tray icon created for %s", busName) +} + +// startStatusNotifierWatcher claims org.kde.StatusNotifierWatcher only as a +// bridge to an XEmbed tray on minimal WMs. The watcher is a stub that never +// relays items to a real StatusNotifierHost, so claiming the name on a desktop +// with a real host (e.g. Hyprland + Waybar) would dead-end every other tray +// app's icon. It gates on the actual presence of an XEmbed tray rather than +// GetNameOwner, which can't win a login-order race; without one it stays off +// the bus so the real watcher owns the name. The XEmbed tray may come up after +// the UI, so it re-probes for a grace period rather than deciding once. +// Safe to call unconditionally. +func startStatusNotifierWatcher() { + go func() { + deadline := time.Now().Add(watcherProbeTimeout) + for { + if xembedTrayAvailable() { + claimStatusNotifierWatcher() + return + } + if time.Now().After(deadline) { + log.Debugf("StatusNotifierWatcher: no XEmbed tray appeared within %s, leaving the watcher to the desktop", watcherProbeTimeout) + return + } + time.Sleep(watcherProbeInterval) + } + }() +} + +// claimStatusNotifierWatcher takes ownership of org.kde.StatusNotifierWatcher +// on a private session bus and exports the stub watcher. The GetNameOwner / +// DoNotQueue guards back off if a real watcher already holds the name. +func claimStatusNotifierWatcher() { + conn, err := dbus.SessionBusPrivate() + if err != nil { + log.Debugf("StatusNotifierWatcher: cannot open private session bus: %v", err) + return + } + if err := conn.Auth(nil); err != nil { + log.Debugf("StatusNotifierWatcher: auth failed: %v", err) + closeBus(conn) + return + } + if err := conn.Hello(); err != nil { + log.Debugf("StatusNotifierWatcher: Hello failed: %v", err) + closeBus(conn) + return + } + + var owner string + callErr := conn.BusObject().Call("org.freedesktop.DBus.GetNameOwner", 0, watcherName).Store(&owner) + if callErr == nil && owner != "" { + log.Debugf("StatusNotifierWatcher: already owned by %s, skipping", owner) + closeBus(conn) + return + } + + reply, err := conn.RequestName(watcherName, dbus.NameFlagDoNotQueue) + if err != nil || reply != dbus.RequestNameReplyPrimaryOwner { + log.Debugf("StatusNotifierWatcher: could not claim name (reply=%v err=%v)", reply, err) + closeBus(conn) + return + } + + w := &statusNotifierWatcher{ + conn: conn, + hosts: make(map[string]*xembedHost), + } + if err := conn.ExportAll(w, dbus.ObjectPath(watcherPath), watcherIface); err != nil { + log.Errorf("StatusNotifierWatcher: export failed: %v", err) + closeBus(conn) + return + } + + log.Infof("StatusNotifierWatcher: active on session bus (enables tray on minimal WMs)") + // Connection kept open for the process lifetime. +} + +// closeBus closes a private session bus opened on a back-off path, logging a +// warning rather than swallowing the error. +func closeBus(conn *dbus.Conn) { + if err := conn.Close(); err != nil { + log.Warnf("StatusNotifierWatcher: closing session bus failed: %v", err) + } +} diff --git a/client/ui/tray_watcher_other.go b/client/ui/tray_watcher_other.go new file mode 100644 index 000000000..ef8a1bb52 --- /dev/null +++ b/client/ui/tray_watcher_other.go @@ -0,0 +1,9 @@ +//go:build (!linux || (linux && 386)) && !freebsd && !android && !ios && !js + +package main + +// startStatusNotifierWatcher is a no-op stub so main.go can call it across all +// build targets; only minimal Linux WMs need the real watcher (tray_watcher_linux.go). +func startStatusNotifierWatcher() { + // Intentionally empty: only minimal Linux WMs need the real SNI watcher. +} diff --git a/client/ui/uilogpath.go b/client/ui/uilogpath.go new file mode 100644 index 000000000..96fbb9637 --- /dev/null +++ b/client/ui/uilogpath.go @@ -0,0 +1,37 @@ +//go:build !android && !ios && !freebsd && !js + +package main + +import ( + "os" + "path/filepath" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/ui/guilog" +) + +// uiLogFileName must stay in sync with the daemon's "gui-client*.log.*" glob +// for rotated siblings (addUILog in client/internal/debug). +const uiLogFileName = "gui-client.log" + +// uiLogPath returns the GUI log path with native separators, since the daemon +// opens it directly for debug-bundle collection. +func uiLogPath() (string, error) { + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "netbird", uiLogFileName), nil +} + +// newDebugLog builds the GUI debug log, disabled when userSetLogFile is set +// (manual --log-file override) or the config dir can't be resolved. +func newDebugLog(userSetLogFile bool) *guilog.DebugLog { + path, err := uiLogPath() + if err != nil { + log.Warnf("resolve GUI log path: %v; GUI file logging disabled", err) + return guilog.NewDebugLog("", false) + } + return guilog.NewDebugLog(path, !userSetLogFile) +} diff --git a/client/ui/update.go b/client/ui/update.go deleted file mode 100644 index 25c317bdf..000000000 --- a/client/ui/update.go +++ /dev/null @@ -1,140 +0,0 @@ -//go:build !(linux && 386) - -package main - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" - log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/client/proto" -) - -func (s *serviceClient) showUpdateProgress(ctx context.Context, version string) { - log.Infof("show installer progress window: %s", version) - s.wUpdateProgress = s.app.NewWindow("Automatically updating client") - - statusLabel := widget.NewLabel("Updating...") - infoLabel := widget.NewLabel(fmt.Sprintf("Your client version is older than the auto-update version set in Management.\nUpdating client to: %s.", version)) - content := container.NewVBox(infoLabel, statusLabel) - s.wUpdateProgress.SetContent(content) - s.wUpdateProgress.CenterOnScreen() - s.wUpdateProgress.SetFixedSize(true) - s.wUpdateProgress.SetCloseIntercept(func() { - // this is empty to lock window until result known - }) - s.wUpdateProgress.RequestFocus() - s.wUpdateProgress.Show() - - updateWindowCtx, cancel := context.WithTimeout(ctx, 15*time.Minute) - - // Initialize dot updater - updateText := dotUpdater() - - // Channel to receive the result from RPC call - resultErrCh := make(chan error, 1) - resultOkCh := make(chan struct{}, 1) - - // Start RPC call in background - go func() { - conn, err := s.getSrvClient(defaultFailTimeout) - if err != nil { - log.Infof("backend not reachable, upgrade in progress: %v", err) - close(resultOkCh) - return - } - - resp, err := conn.GetInstallerResult(updateWindowCtx, &proto.InstallerResultRequest{}) - if err != nil { - log.Infof("backend stopped responding, upgrade in progress: %v", err) - close(resultOkCh) - return - } - - if !resp.Success { - resultErrCh <- mapInstallError(resp.ErrorMsg) - return - } - - // Success - close(resultOkCh) - }() - - // Update UI with dots and wait for result - go func() { - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - defer cancel() - - // allow closing update window after 10 sec - timerResetCloseInterceptor := time.NewTimer(10 * time.Second) - defer timerResetCloseInterceptor.Stop() - - for { - select { - case <-updateWindowCtx.Done(): - s.showInstallerResult(statusLabel, updateWindowCtx.Err()) - return - case err := <-resultErrCh: - s.showInstallerResult(statusLabel, err) - return - case <-resultOkCh: - log.Info("backend exited, upgrade in progress, closing all UI") - killParentUIProcess() - s.app.Quit() - return - case <-ticker.C: - statusLabel.SetText(updateText()) - case <-timerResetCloseInterceptor.C: - s.wUpdateProgress.SetCloseIntercept(nil) - } - } - }() -} - -func (s *serviceClient) showInstallerResult(statusLabel *widget.Label, err error) { - s.wUpdateProgress.SetCloseIntercept(nil) - switch { - case errors.Is(err, context.DeadlineExceeded): - log.Warn("update watcher timed out") - statusLabel.SetText("Update timed out. Please try again.") - case errors.Is(err, context.Canceled): - log.Info("update watcher canceled") - statusLabel.SetText("Update canceled.") - case err != nil: - log.Errorf("update failed: %v", err) - statusLabel.SetText("Update failed: " + err.Error()) - default: - s.wUpdateProgress.Close() - } -} - -// dotUpdater returns a closure that cycles through dots for a loading animation. -func dotUpdater() func() string { - dotCount := 0 - return func() string { - dotCount = (dotCount + 1) % 4 - return fmt.Sprintf("%s%s", "Updating", strings.Repeat(".", dotCount)) - } -} - -func mapInstallError(msg string) error { - msg = strings.ToLower(strings.TrimSpace(msg)) - - switch { - case strings.Contains(msg, "deadline exceeded"), strings.Contains(msg, "timeout"): - return context.DeadlineExceeded - case strings.Contains(msg, "canceled"), strings.Contains(msg, "cancelled"): - return context.Canceled - case msg == "": - return errors.New("unknown update error") - default: - return errors.New(msg) - } -} diff --git a/client/ui/update_notwindows.go b/client/ui/update_notwindows.go deleted file mode 100644 index 5766f18f7..000000000 --- a/client/ui/update_notwindows.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !windows && !(linux && 386) - -package main - -func killParentUIProcess() { - // No-op on non-Windows platforms -} diff --git a/client/ui/update_windows.go b/client/ui/update_windows.go deleted file mode 100644 index 1b03936f9..000000000 --- a/client/ui/update_windows.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build windows - -package main - -import ( - log "github.com/sirupsen/logrus" - "golang.org/x/sys/windows" - - nbprocess "github.com/netbirdio/netbird/client/ui/process" -) - -// killParentUIProcess finds and kills the parent systray UI process on Windows. -// This is a workaround in case the MSI installer fails to properly terminate the UI process. -// The installer should handle this via util:CloseApplication with TerminateProcess, but this -// provides an additional safety mechanism to ensure the UI is closed before the upgrade proceeds. -func killParentUIProcess() { - pid, running, err := nbprocess.IsAnotherProcessRunning() - if err != nil { - log.Warnf("failed to check for parent UI process: %v", err) - return - } - - if !running { - log.Debug("no parent UI process found to kill") - return - } - - log.Infof("killing parent UI process (PID: %d)", pid) - - // Open the process with terminate rights - handle, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, uint32(pid)) - if err != nil { - log.Warnf("failed to open parent process %d: %v", pid, err) - return - } - defer func() { - _ = windows.CloseHandle(handle) - }() - - // Terminate the process with exit code 0 - if err := windows.TerminateProcess(handle, 0); err != nil { - log.Warnf("failed to terminate parent process %d: %v", pid, err) - } -} diff --git a/client/ui/updater/state.go b/client/ui/updater/state.go new file mode 100644 index 000000000..606f8b401 --- /dev/null +++ b/client/ui/updater/state.go @@ -0,0 +1,97 @@ +//go:build !android && !ios && !freebsd && !js + +// Package updater holds the auto-update domain: the typed State, the +// daemon-SystemEvent metadata schema, and the Holder that caches the latest +// state and broadcasts changes. No Wails dependency. +package updater + +import ( + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/proto" +) + +// EventStateChanged carries the full State snapshot as payload. +const EventStateChanged = "netbird:update:state" + +// State is the typed snapshot of the daemon's update situation. Installing is +// driven only by the daemon's progress_window:show event; a UI-side +// Update.Trigger() does not flip it. +type State struct { + Available bool `json:"available"` + Version string `json:"version"` + Enforced bool `json:"enforced"` + Installing bool `json:"installing"` +} + +// Emitter is the broadcast dependency Holder needs; the Wails app.Event +// processor satisfies it. +type Emitter interface { + Emit(name string, data ...any) bool +} + +// Holder caches the latest update State and broadcasts changes. +type Holder struct { + emitter Emitter + + mu sync.Mutex + state State +} + +// NewHolder constructs an empty-state Holder. A nil emitter skips the broadcast. +func NewHolder(emitter Emitter) *Holder { + return &Holder{emitter: emitter} +} + +// Get returns a copy of the cached State. +func (h *Holder) Get() State { + h.mu.Lock() + defer h.mu.Unlock() + return h.state +} + +// OnSystemEvent folds update-related metadata into the cached state, emitting +// EventStateChanged only on an actual change so repeated daemon snapshots +// don't produce redundant pushes. +func (h *Holder) OnSystemEvent(ev *proto.SystemEvent) { + md := ev.GetMetadata() + if len(md) == 0 { + return + } + + h.mu.Lock() + changed := false + if v, ok := md["new_version_available"]; ok { + _, enforced := md["enforced"] + if !h.state.Available || h.state.Version != v || h.state.Enforced != enforced { + h.state.Available = true + h.state.Version = v + h.state.Enforced = enforced + changed = true + } + } + if md["progress_window"] == "show" { + if !h.state.Installing { + h.state.Installing = true + changed = true + } + if v, ok := md["version"]; ok && v != "" && h.state.Version != v { + h.state.Version = v + h.state.Available = true + changed = true + } + } + snap := h.state + h.mu.Unlock() + + if !changed { + return + } + log.Infof("update state: available=%v version=%q enforced=%v installing=%v", + snap.Available, snap.Version, snap.Enforced, snap.Installing) + if h.emitter != nil { + h.emitter.Emit(EventStateChanged, snap) + } +} diff --git a/client/ui/xembed_host_linux.go b/client/ui/xembed_host_linux.go new file mode 100644 index 000000000..ba7a5d07c --- /dev/null +++ b/client/ui/xembed_host_linux.go @@ -0,0 +1,443 @@ +//go:build linux && !(linux && 386) + +package main + +/* +#cgo pkg-config: x11 gtk4 gtk4-x11 cairo cairo-xlib +#cgo LDFLAGS: -lX11 +#include "xembed_tray_linux.h" +#include +#include +#include +*/ +import "C" + +import ( + "errors" + "sync" + "time" + "unsafe" + + "github.com/godbus/dbus/v5" + log "github.com/sirupsen/logrus" +) + +// activeMenuHost holds the popup owner; C callbacks cannot carry Go pointers. +var ( + activeMenuHost *xembedHost + activeMenuHostMu sync.Mutex +) + +// menuItemInfo is a dbusMenuLayout entry flattened for the C popup builder. +type menuItemInfo struct { + id int32 + label string + enabled bool + isCheck bool + checked bool + isSeparator bool + children []menuItemInfo +} + +// dbusMenuLayout mirrors the (ia{sv}av) result of com.canonical.dbusmenu.GetLayout. +// Each Children variant wraps a nested dbusMenuLayout, decoded in flattenMenu. +type dbusMenuLayout struct { + ID int32 + Properties map[string]dbus.Variant + Children []dbus.Variant +} + +// xembedHost manages one XEmbed tray icon for an SNI item. +type xembedHost struct { + conn *dbus.Conn + busName string + objPath dbus.ObjectPath + + dpy *C.Display + trayMgr C.Window + iconWin C.Window + iconSize int + + mu sync.Mutex + iconData []byte + iconW int + iconH int + + stopCh chan struct{} +} + +// newXembedHost creates an XEmbed tray icon for the given SNI item. +// Errors when no XEmbed tray manager is available, so callers can fall back. +func newXembedHost(conn *dbus.Conn, busName string, objPath dbus.ObjectPath) (*xembedHost, error) { + dpy := C.XOpenDisplay(nil) + if dpy == nil { + return nil, errors.New("cannot open X display") + } + C.xembed_install_error_handlers() + + screen := C.xembed_default_screen(dpy) + trayMgr := C.xembed_find_tray(dpy, screen) + if trayMgr == 0 { + C.XCloseDisplay(dpy) + return nil, errors.New("no XEmbed system tray found") + } + + iconSize := int(C.xembed_get_icon_size(dpy, trayMgr)) + if iconSize <= 0 { + iconSize = 24 // fallback + } + + iconWin := C.xembed_create_icon(dpy, screen, C.int(iconSize), trayMgr) + if iconWin == 0 { + C.XCloseDisplay(dpy) + return nil, errors.New("failed to create icon window") + } + + if C.xembed_dock(dpy, trayMgr, iconWin) != 0 { + C.xembed_destroy_icon(dpy, iconWin) + C.XCloseDisplay(dpy) + return nil, errors.New("failed to dock icon") + } + + h := &xembedHost{ + conn: conn, + busName: busName, + objPath: objPath, + dpy: dpy, + trayMgr: trayMgr, + iconWin: iconWin, + iconSize: iconSize, + stopCh: make(chan struct{}), + } + + h.fetchAndDrawIcon() + return h, nil +} + +func (h *xembedHost) fetchAndDrawIcon() { + obj := h.conn.Object(h.busName, h.objPath) + variant, err := obj.GetProperty("org.kde.StatusNotifierItem.IconPixmap") + if err != nil { + log.Debugf("xembed: failed to get IconPixmap: %v", err) + return + } + + // IconPixmap has D-Bus signature a(iiay). + type px struct { + W int32 + H int32 + Pix []byte + } + + var icons []px + if err := variant.Store(&icons); err != nil { + log.Debugf("xembed: failed to decode IconPixmap: %v", err) + return + } + + if len(icons) == 0 { + log.Debug("xembed: IconPixmap is empty") + return + } + + icon := icons[0] + if icon.W <= 0 || icon.H <= 0 || len(icon.Pix) < int(icon.W*icon.H*4) { + log.Debug("xembed: invalid IconPixmap data") + return + } + + h.mu.Lock() + h.iconData = icon.Pix + h.iconW = int(icon.W) + h.iconH = int(icon.H) + h.mu.Unlock() + + h.drawIcon() +} + +func (h *xembedHost) drawIcon() { + h.mu.Lock() + data := h.iconData + w := h.iconW + ht := h.iconH + h.mu.Unlock() + + if data == nil || w <= 0 || ht <= 0 { + return + } + + cData := C.CBytes(data) + defer C.free(cData) + + C.xembed_draw_icon(h.dpy, h.iconWin, C.int(h.iconSize), + (*C.uchar)(cData), C.int(w), C.int(ht)) +} + +// run is the event loop: polls X11 events and D-Bus NewIcon signals until stopped. +func (h *xembedHost) run() { + matchRule := "type='signal',interface='org.kde.StatusNotifierItem',member='NewIcon',sender='" + h.busName + "'" + if err := h.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err; err != nil { + log.Debugf("xembed: failed to add signal match: %v", err) + } + + sigCh := make(chan *dbus.Signal, 16) + h.conn.Signal(sigCh) + defer h.conn.RemoveSignal(sigCh) + + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-h.stopCh: + return + + case sig := <-sigCh: + if sig == nil { + continue + } + if sig.Name == "org.kde.StatusNotifierItem.NewIcon" { + h.fetchAndDrawIcon() + } + + case <-ticker.C: + var outX, outY C.int + result := C.xembed_poll_event(h.dpy, h.iconWin, &outX, &outY) + + switch result { + case 1: // left click + go h.activate(int32(outX), int32(outY)) + case 2: // right click + go h.contextMenu(int32(outX), int32(outY)) + case 3: // expose + h.drawIcon() + case 4: // configure (resize) + newSize := int(outX) + if newSize > 0 && newSize != h.iconSize { + h.iconSize = newSize + h.drawIcon() + } + case -1: // tray died + log.Info("xembed: tray manager destroyed, cleaning up") + return + } + } + } +} + +func (h *xembedHost) activate(x, y int32) { + obj := h.conn.Object(h.busName, h.objPath) + if err := obj.Call("org.kde.StatusNotifierItem.Activate", 0, x, y).Err; err != nil { + log.Debugf("xembed: Activate call failed: %v", err) + } +} + +func (h *xembedHost) contextMenu(x, y int32) { + menuPath := dbus.ObjectPath("/StatusNotifierMenu") + + menuObj := h.conn.Object(h.busName, menuPath) + var revision uint32 + var layout dbusMenuLayout + err := menuObj.Call("com.canonical.dbusmenu.GetLayout", 0, + int32(0), // parentId (root) + int32(-1), // recursionDepth (all) + []string{}, // propertyNames (all) + ).Store(&revision, &layout) + if err != nil { + log.Debugf("xembed: GetLayout failed: %v", err) + return + } + + items := h.flattenMenu(layout) + log.Debugf("xembed: menu has %d items (revision %d)", len(items), revision) + if len(items) == 0 { + return + } + + var allocs []unsafe.Pointer + cItems := buildCItems(items, &allocs) + defer func() { + for _, p := range allocs { + C.free(p) + } + }() + + // C callback reaches us through this global. + activeMenuHostMu.Lock() + activeMenuHost = h + activeMenuHostMu.Unlock() + + C.xembed_show_popup_menu(cItems, C.int(len(items)), + nil, C.int(x), C.int(y)) +} + +func (h *xembedHost) flattenMenu(layout dbusMenuLayout) []menuItemInfo { + var items []menuItemInfo + + for _, childVar := range layout.Children { + var child dbusMenuLayout + if err := dbus.Store([]interface{}{childVar.Value()}, &child); err != nil { + continue + } + if mi, ok := h.menuItemFromLayout(child); ok { + items = append(items, mi) + } + } + + return items +} + +// menuItemFromLayout decodes one dbusmenu child; ok is false for hidden items (drop them). +func (h *xembedHost) menuItemFromLayout(child dbusMenuLayout) (menuItemInfo, bool) { + mi := menuItemInfo{id: child.ID, enabled: true} + + if propString(child.Properties, "type") == "separator" { + mi.isSeparator = true + return mi, true + } + + if vis, ok := propBool(child.Properties, "visible"); ok && !vis { + return menuItemInfo{}, false + } + + mi.label = propString(child.Properties, "label") + if en, ok := propBool(child.Properties, "enabled"); ok { + mi.enabled = en + } + if propString(child.Properties, "toggle-type") == "checkmark" { + mi.isCheck = true + } + if n, ok := propInt32(child.Properties, "toggle-state"); ok && n == 1 { + mi.checked = true + } + + // children are already present from the recursionDepth=-1 GetLayout. + if propString(child.Properties, "children-display") == "submenu" { + mi.children = h.flattenMenu(child) + } + + return mi, true +} + +func (h *xembedHost) sendMenuEvent(id int32) { + menuPath := dbus.ObjectPath("/StatusNotifierMenu") + menuObj := h.conn.Object(h.busName, menuPath) + data := dbus.MakeVariant("") + err := menuObj.Call("com.canonical.dbusmenu.Event", 0, + id, "clicked", data, uint32(0)).Err + if err != nil { + log.Debugf("xembed: menu Event call failed: %v", err) + } +} + +func (h *xembedHost) stop() { + select { + case <-h.stopCh: + return + default: + close(h.stopCh) + } + + C.xembed_destroy_icon(h.dpy, h.iconWin) + C.XCloseDisplay(h.dpy) +} + +// buildCItems builds a C-allocated xembed_menu_item tree. Every malloc is +// appended to *allocs for the caller to free once the C side has deep-copied it. +func buildCItems(items []menuItemInfo, allocs *[]unsafe.Pointer) *C.xembed_menu_item { + if len(items) == 0 { + return nil + } + size := C.size_t(len(items)) * C.size_t(unsafe.Sizeof(C.xembed_menu_item{})) + arr := C.malloc(size) + *allocs = append(*allocs, arr) + C.memset(arr, 0, size) + + slice := (*[1 << 16]C.xembed_menu_item)(arr)[:len(items):len(items)] + for i, mi := range items { + slice[i].id = C.int(mi.id) + slice[i].enabled = boolToInt(mi.enabled) + slice[i].is_check = boolToInt(mi.isCheck) + slice[i].checked = boolToInt(mi.checked) + slice[i].is_separator = boolToInt(mi.isSeparator) + if mi.label != "" { + cstr := C.CString(mi.label) + *allocs = append(*allocs, unsafe.Pointer(cstr)) + slice[i].label = cstr + } + if len(mi.children) > 0 { + slice[i].children = buildCItems(mi.children, allocs) + slice[i].child_count = C.int(len(mi.children)) + } + } + + return (*C.xembed_menu_item)(arr) +} + +// xembedTrayAvailable reports whether an XEmbed tray manager (_NET_SYSTEM_TRAY_S0) +// owns the default screen. Side-effect-free probe. Gates the in-process +// StatusNotifierWatcher: when a real SNI host already owns the tray (e.g. Waybar +// on Wayland) we must not claim org.kde.StatusNotifierWatcher and shadow it. +// Returns false when there is no X display (pure Wayland). +func xembedTrayAvailable() bool { + dpy := C.XOpenDisplay(nil) + if dpy == nil { + return false + } + C.xembed_install_error_handlers() + defer C.XCloseDisplay(dpy) + screen := C.xembed_default_screen(dpy) + return C.xembed_find_tray(dpy, screen) != 0 +} + +// goMenuItemClicked is the C callback fired from the GTK main thread on popup +// activation. The host is looked up via activeMenuHost since C callbacks can't +// carry Go pointers; //export requires this to live in package main. +// +//export goMenuItemClicked +func goMenuItemClicked(id C.int) { + activeMenuHostMu.Lock() + h := activeMenuHost + activeMenuHostMu.Unlock() + + if h != nil { + go h.sendMenuEvent(int32(id)) + } +} + +func boolToInt(b bool) C.int { + if b { + return 1 + } + return 0 +} + +// propString returns the property's string value, or "" if absent or not a string. +func propString(props map[string]dbus.Variant, key string) string { + if v, ok := props[key]; ok { + if s, ok := v.Value().(string); ok { + return s + } + } + return "" +} + +// propBool returns the property's bool value; ok is false if absent or not a bool. +func propBool(props map[string]dbus.Variant, key string) (value, ok bool) { + if v, present := props[key]; present { + if b, isBool := v.Value().(bool); isBool { + return b, true + } + } + return false, false +} + +// propInt32 returns the property's int32 value; ok is false if absent or not an int32. +func propInt32(props map[string]dbus.Variant, key string) (value int32, ok bool) { + if v, present := props[key]; present { + if n, isInt := v.Value().(int32); isInt { + return n, true + } + } + return 0, false +} diff --git a/client/ui/xembed_tray_linux.c b/client/ui/xembed_tray_linux.c new file mode 100644 index 000000000..07ec74bb2 --- /dev/null +++ b/client/ui/xembed_tray_linux.c @@ -0,0 +1,708 @@ +#include "xembed_tray_linux.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SYSTEM_TRAY_REQUEST_DOCK 0 +#define XEMBED_MAPPED (1 << 0) + +/* Xlib's default protocol-error handler calls exit() on any async X error, + killing the whole UI process. Handlers are process-global (not + per-Display), so a single install covers our raw tray Display and GDK's. + Tray work is full of races the X server reports asynchronously — the tray + manager window dying between xembed_find_tray and xembed_dock (BadWindow + on XSendEvent), or x11_move_window touching a popup the WM already + destroyed — and the default handler would take us down for any of them. + Returning 0 here makes the error a logged no-op instead. */ +static int xembed_x_error_handler(Display *dpy, XErrorEvent *ev) { + char buf[256]; + XGetErrorText(dpy, ev->error_code, buf, sizeof(buf)); + fprintf(stderr, + "xembed: X error (ignored): %s (code=%d, request=%d.%d, resource=0x%lx)\n", + buf, ev->error_code, ev->request_code, ev->minor_code, + ev->resourceid); + return 0; +} + +/* The I/O error handler fires when the X connection itself drops (server + gone, socket closed). Xlib treats this as fatal and exits even if we + return, so this can't keep the process alive — it only logs a clearer + line than Xlib's terse default before the unavoidable exit. */ +static int xembed_x_io_error_handler(Display *dpy) { + (void)dpy; + fprintf(stderr, "xembed: X I/O error (connection lost)\n"); + return 0; +} + +/* Install the process-global handlers. Idempotent and cheap, so callers may + invoke it after every XOpenDisplay without tracking prior installs. */ +void xembed_install_error_handlers(void) { + XSetErrorHandler(xembed_x_error_handler); + XSetIOErrorHandler(xembed_x_io_error_handler); +} + +Window xembed_find_tray(Display *dpy, int screen) { + char atom_name[64]; + snprintf(atom_name, sizeof(atom_name), "_NET_SYSTEM_TRAY_S%d", screen); + Atom sel = XInternAtom(dpy, atom_name, False); + return XGetSelectionOwner(dpy, sel); +} + +int xembed_get_icon_size(Display *dpy, Window tray_mgr) { + Atom atom = XInternAtom(dpy, "_NET_SYSTEM_TRAY_ICON_SIZE", False); + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *prop = NULL; + int size = 0; + + if (XGetWindowProperty(dpy, tray_mgr, atom, 0, 1, False, + XA_CARDINAL, &actual_type, &actual_format, + &nitems, &bytes_after, &prop) == Success) { + if (prop && nitems == 1 && actual_format == 32) { + size = (int)(*(unsigned long *)prop); + } + if (prop) + XFree(prop); + } + return size; +} + +Window xembed_create_icon(Display *dpy, int screen, int size, + Window tray_mgr) { + (void)tray_mgr; /* unused; kept in signature for caller symmetry */ + Window root = RootWindow(dpy, screen); + + /* Inherit visual & depth from the parent (tray manager / root) so + ParentRelative background works on every tray. Many minimal + toolbars (Fluxbox slit, OpenBox, etc.) only offer a 24-bit + default visual and do not composite alpha; ParentRelative makes + the X server texture this window's background from the parent, + so transparent pixels in the icon show the toolbar beneath + instead of solid black. ARGB-aware trays still work because the + cairo OVER blend in xembed_draw_icon honours per-pixel alpha + against whatever base the X server painted underneath. */ + XSetWindowAttributes attrs; + memset(&attrs, 0, sizeof(attrs)); + attrs.event_mask = ButtonPressMask | StructureNotifyMask | ExposureMask; + attrs.background_pixmap = ParentRelative; + unsigned long mask = CWEventMask | CWBackPixmap; + + Window win = XCreateWindow( + dpy, root, + 0, 0, size, size, + 0, /* border width */ + CopyFromParent, /* depth */ + InputOutput, + CopyFromParent, /* visual */ + mask, + &attrs + ); + + /* Set _XEMBED_INFO: version=0, flags=XEMBED_MAPPED */ + Atom xembed_info = XInternAtom(dpy, "_XEMBED_INFO", False); + unsigned long info[2] = { 0, XEMBED_MAPPED }; + XChangeProperty(dpy, win, xembed_info, xembed_info, + 32, PropModeReplace, (unsigned char *)info, 2); + + return win; +} + +int xembed_dock(Display *dpy, Window tray_mgr, Window icon_win) { + Atom opcode = XInternAtom(dpy, "_NET_SYSTEM_TRAY_OPCODE", False); + + XClientMessageEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = ClientMessage; + ev.window = tray_mgr; + ev.message_type = opcode; + ev.format = 32; + ev.data.l[0] = CurrentTime; + ev.data.l[1] = SYSTEM_TRAY_REQUEST_DOCK; + ev.data.l[2] = (long)icon_win; + + XSendEvent(dpy, tray_mgr, False, NoEventMask, (XEvent *)&ev); + XFlush(dpy); + return 0; +} + +void xembed_draw_icon(Display *dpy, Window icon_win, int win_size, + const unsigned char *data, int img_w, int img_h) { + if (!data || img_w <= 0 || img_h <= 0 || win_size <= 0) + return; + + /* Query the window's actual visual and depth so cairo composites + through the matching ARGB pipeline. */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dpy, icon_win, &wa)) + return; + + /* Build a CAIRO_FORMAT_ARGB32 source surface from the SNI IconPixmap + bytes. SNI ships the pixels as [A,R,G,B,...] in network byte + order; cairo's ARGB32 stores native uint32 with B in the lowest + byte on little-endian hosts. Repack into native order with + pre-multiplied alpha so cairo can composite without tonemapping. */ + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, img_w); + unsigned char *buf = (unsigned char *)calloc(stride * img_h, 1); + if (!buf) + return; + + for (int y = 0; y < img_h; y++) { + unsigned int *row = (unsigned int *)(buf + y * stride); + for (int x = 0; x < img_w; x++) { + int idx = (y * img_w + x) * 4; + unsigned int a = data[idx + 0]; + unsigned int r = data[idx + 1]; + unsigned int g = data[idx + 2]; + unsigned int b = data[idx + 3]; + + if (a == 0) { + row[x] = 0; + } else if (a == 255) { + row[x] = (a << 24) | (r << 16) | (g << 8) | b; + } else { + unsigned int pr = r * a / 255; + unsigned int pg = g * a / 255; + unsigned int pb = b * a / 255; + row[x] = (a << 24) | (pr << 16) | (pg << 8) | pb; + } + } + } + + cairo_surface_t *src = cairo_image_surface_create_for_data( + buf, CAIRO_FORMAT_ARGB32, img_w, img_h, stride); + if (cairo_surface_status(src) != CAIRO_STATUS_SUCCESS) { + cairo_surface_destroy(src); + free(buf); + return; + } + + /* Wrap the X11 window in a cairo XLib surface using its real visual. */ + cairo_surface_t *dst = cairo_xlib_surface_create( + dpy, icon_win, wa.visual, win_size, win_size); + if (cairo_surface_status(dst) != CAIRO_STATUS_SUCCESS) { + cairo_surface_destroy(dst); + cairo_surface_destroy(src); + free(buf); + return; + } + + /* Repaint the ParentRelative background first — without this the + window keeps the previously-drawn icon underneath when an icon + update arrives, and cairo's OVER blend would composite the new + icon on top of the stale one. XClearWindow forces the X server + to retexture from the parent (tray toolbar), giving us a clean + opaque base. */ + XClearWindow(dpy, icon_win); + + cairo_t *cr = cairo_create(dst); + + /* Scale the source onto the window with alpha compositing (default + OPERATOR_OVER). Transparent pixels keep the toolbar's pixels + visible underneath. */ + double sx = (double)win_size / img_w; + double sy = (double)win_size / img_h; + cairo_scale(cr, sx, sy); + cairo_set_source_surface(cr, src, 0, 0); + cairo_paint(cr); + + cairo_destroy(cr); + cairo_surface_destroy(dst); + cairo_surface_destroy(src); + free(buf); + XFlush(dpy); +} + +void xembed_destroy_icon(Display *dpy, Window icon_win) { + if (icon_win) + XDestroyWindow(dpy, icon_win); + XFlush(dpy); +} + +int xembed_poll_event(Display *dpy, Window icon_win, + int *out_x, int *out_y) { + *out_x = 0; + *out_y = 0; + + while (XPending(dpy) > 0) { + XEvent ev; + XNextEvent(dpy, &ev); + + switch (ev.type) { + case ButtonPress: + if (ev.xbutton.window == icon_win) { + *out_x = ev.xbutton.x_root; + *out_y = ev.xbutton.y_root; + if (ev.xbutton.button == Button1) + return 1; + if (ev.xbutton.button == Button3) + return 2; + } + break; + + case Expose: + if (ev.xexpose.window == icon_win && ev.xexpose.count == 0) + return 3; + break; + + case DestroyNotify: + if (ev.xdestroywindow.window == icon_win) + return -1; + break; + + case ConfigureNotify: + if (ev.xconfigure.window == icon_win) { + *out_x = ev.xconfigure.width; + *out_y = ev.xconfigure.height; + return 4; + } + break; + + case ReparentNotify: + /* Tray manager reparented us — this is expected after docking. */ + break; + + default: + break; + } + } + + return 0; +} + +/* --- GTK4 popup window menu support --- */ + +/* Implemented in Go via //export */ +extern void goMenuItemClicked(int id); + +/* The top-level popup window, reused across invocations. Submenu + popups are tracked in a separate list so they all close when the + top-level closes. */ +static GtkWidget *popup_win = NULL; +static GList *submenu_popups = NULL; /* list of GtkWidget* */ + +typedef struct { + xembed_menu_item *items; + int count; + int x, y; +} popup_data; + +/* Deep-free a heap-owned xembed_menu_item array (label + children). */ +static void free_items(xembed_menu_item *items, int count) { + if (!items) return; + for (int i = 0; i < count; i++) { + free((void *)items[i].label); + free_items(items[i].children, items[i].child_count); + } + free(items); +} + +static void free_popup_data(popup_data *pd) { + if (!pd) return; + free_items(pd->items, pd->count); + free(pd); +} + + +/* Close every popup window — top-level plus any open submenus. + Called when the user clicks an actionable item or focus leaves the + menu tree. */ +static void close_all_popups(void) { + for (GList *l = submenu_popups; l; l = l->next) { + gtk_window_destroy(GTK_WINDOW(l->data)); + } + g_list_free(submenu_popups); + submenu_popups = NULL; + + if (popup_win) { + gtk_widget_set_visible(popup_win, FALSE); + } +} + +static void on_button_clicked(GtkButton *btn, gpointer user_data) { + (void)btn; + int id = GPOINTER_TO_INT(user_data); + close_all_popups(); + goMenuItemClicked(id); +} + +static void on_check_toggled(GtkCheckButton *btn, gpointer user_data) { + (void)btn; + int id = GPOINTER_TO_INT(user_data); + close_all_popups(); + goMenuItemClicked(id); +} + +/* The popup is a regular WM-managed window (not override-redirect), + so the WM hands keyboard focus to it on map. When focus moves + elsewhere — the user clicked somewhere else, switched apps, etc. — + the focus controller's "leave" signal fires and we tear down the + menu tree. Submenus open from inside the top-level popup, so we + defer the actual close to an idle callback: that gives the new + submenu a chance to take focus first, and we only close if none of + our windows still has it. */ +static gboolean any_popup_has_focus(void) { + if (popup_win && gtk_window_is_active(GTK_WINDOW(popup_win))) + return TRUE; + for (GList *l = submenu_popups; l; l = l->next) { + if (gtk_window_is_active(GTK_WINDOW(l->data))) + return TRUE; + } + return FALSE; +} + +static gboolean focus_out_recheck(gpointer user_data) { + (void)user_data; + if (!any_popup_has_focus()) + close_all_popups(); + return G_SOURCE_REMOVE; +} + +static void on_popup_focus_leave(GtkEventControllerFocus *ctrl, + gpointer user_data) { + (void)ctrl; (void)user_data; + g_idle_add(focus_out_recheck, NULL); +} + +/* Attach a focus controller that fires close_all_popups on focus loss. */ +static void attach_outside_click_close(GtkWidget *win) { + GtkEventController *focus = gtk_event_controller_focus_new(); + g_signal_connect(focus, "leave", + G_CALLBACK(on_popup_focus_leave), NULL); + gtk_widget_add_controller(win, focus); +} + +/* Move a GtkWindow at the X11 level. GTK4 removed gtk_window_move(); the + GdkSurface is mapped to a real X11 Window we can reposition with + XMoveWindow. Must be called after the window has been realized (i.e. + after gtk_widget_set_visible TRUE). + + The popup is **not** override-redirect — the WM keeps managing it so + focus tracking still works (focus-out fires when the user clicks + elsewhere). We tag the window with a stack of EWMH hints that make + sane WMs (fluxbox, openbox, i3, kwin, mutter) render it like a + floating menu: above the tray panel, skipped from taskbar/pager, + no decorations. */ +static void x11_move_window(GtkWidget *win, int x, int y) { + GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(win)); + if (!surface || !GDK_IS_X11_SURFACE(surface)) + return; + Window xid = gdk_x11_surface_get_xid(surface); + GdkDisplay *display = gdk_surface_get_display(surface); + Display *xdpy = gdk_x11_display_get_xdisplay(GDK_X11_DISPLAY(display)); + + /* These calls poke a window the WM may have already destroyed (a popup + torn down between scheduling and this idle callback). On GDK's Display + use GDK's own error trap rather than our global handler — push/pop is + the spec-correct way to make untrapped BadWindow/BadMatch from these + raw Xlib calls non-fatal, independent of whichever process-global + handler happens to be installed. */ + gdk_x11_display_error_trap_push(display); + + /* _NET_WM_WINDOW_TYPE_POPUP_MENU: makes fluxbox / openbox / etc + render the window above panels and skip decorations. Must be + set before the window is mapped to be honoured by some WMs; + on already-mapped windows it works for most modern WMs but a + few need an unmap/map cycle to re-read the property. */ + Atom wm_type = XInternAtom(xdpy, "_NET_WM_WINDOW_TYPE", False); + Atom wm_type_popup = XInternAtom(xdpy, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False); + XChangeProperty(xdpy, xid, wm_type, XA_ATOM, 32, + PropModeReplace, (unsigned char *)&wm_type_popup, 1); + + /* _NET_WM_STATE_ABOVE + SKIP_TASKBAR + SKIP_PAGER. Bundled into + one property write. */ + Atom wm_state = XInternAtom(xdpy, "_NET_WM_STATE", False); + Atom state_above = XInternAtom(xdpy, "_NET_WM_STATE_ABOVE", False); + Atom state_skip_tb = XInternAtom(xdpy, "_NET_WM_STATE_SKIP_TASKBAR", False); + Atom state_skip_pg = XInternAtom(xdpy, "_NET_WM_STATE_SKIP_PAGER", False); + Atom states[3] = { state_above, state_skip_tb, state_skip_pg }; + XChangeProperty(xdpy, xid, wm_state, XA_ATOM, 32, + PropModeReplace, (unsigned char *)states, 3); + + XMoveWindow(xdpy, xid, x, y); + XRaiseWindow(xdpy, xid); + + /* POPUP_MENU windows aren't given keyboard focus by most WMs (the + spec says they're "menus", which traditionally use a grab rather + than focus). Without focus GtkEventControllerFocus's leave signal + never fires, so we'd have no way to notice the user clicking + elsewhere. Ask the WM to activate us via _NET_ACTIVE_WINDOW + (source=2 means "pager / pseudo-user request" which most WMs + honour without timestamp checks). This is safer than calling + XSetInputFocus directly — that races the X server with the + not-yet-fully-mapped window and trips BadMatch. */ + Atom net_active = XInternAtom(xdpy, "_NET_ACTIVE_WINDOW", False); + XClientMessageEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = ClientMessage; + ev.window = xid; + ev.message_type = net_active; + ev.format = 32; + ev.data.l[0] = 2; /* source: pager */ + ev.data.l[1] = CurrentTime; + XSendEvent(xdpy, DefaultRootWindow(xdpy), False, + SubstructureRedirectMask | SubstructureNotifyMask, + (XEvent *)&ev); + + XFlush(xdpy); + gdk_x11_display_error_trap_pop_ignored(display); +} + +/* Forward declaration — submenu buttons need to schedule a child popup. */ +static GtkWidget *build_menu_box(xembed_menu_item *items, int count); + +typedef struct { + xembed_menu_item *items; + int count; + GtkWidget *anchor; /* the submenu button — used to position the popup */ +} submenu_open_data; + +static void on_submenu_button_clicked(GtkButton *btn, gpointer user_data) { + submenu_open_data *sd = (submenu_open_data *)user_data; + + GtkWidget *win = gtk_window_new(); + gtk_window_set_decorated(GTK_WINDOW(win), FALSE); + gtk_window_set_resizable(GTK_WINDOW(win), FALSE); + + attach_outside_click_close(win); + + GtkWidget *vbox = build_menu_box(sd->items, sd->count); + gtk_window_set_child(GTK_WINDOW(win), vbox); + + /* Need the anchor button's position in root coordinates. GTK4 + removed gtk_widget_translate_coordinates(); compute via the + button's bounds within its native widget plus the native + surface's screen origin via X11. */ + graphene_rect_t bounds; + if (!gtk_widget_compute_bounds(GTK_WIDGET(btn), + GTK_WIDGET(gtk_widget_get_native(GTK_WIDGET(btn))), + &bounds)) { + bounds.origin.x = 0; + bounds.origin.y = 0; + bounds.size.width = 0; + bounds.size.height = 0; + } + GdkSurface *anchor_surface = + gtk_native_get_surface(gtk_widget_get_native(GTK_WIDGET(btn))); + int ox = 0, oy = 0; + if (anchor_surface && GDK_IS_X11_SURFACE(anchor_surface)) { + Window axid = gdk_x11_surface_get_xid(anchor_surface); + GdkDisplay *display = gdk_surface_get_display(anchor_surface); + Display *xdpy = gdk_x11_display_get_xdisplay(GDK_X11_DISPLAY(display)); + Window child; + /* Trap BadWindow in case the anchor's surface is torn down between + the click and this handler running. */ + gdk_x11_display_error_trap_push(display); + XTranslateCoordinates(xdpy, axid, DefaultRootWindow(xdpy), + 0, 0, &ox, &oy, &child); + gdk_x11_display_error_trap_pop_ignored(display); + } + int ax = ox + (int)bounds.origin.x; + int ay = oy + (int)bounds.origin.y; + + gtk_widget_set_visible(win, TRUE); + + int sw, sh; + gtk_window_get_default_size(GTK_WINDOW(win), &sw, &sh); + if (sw <= 0 || sh <= 0) { + /* default_size returns -1,-1 if never explicitly set; fall back + to the measured preferred size. */ + GtkRequisition req; + gtk_widget_get_preferred_size(win, NULL, &req); + sw = req.width; + sh = req.height; + } + + /* The parent popup grows upward from the tray, so submenu items + sit closer to the bottom of the screen than to the top. Align + the submenu's BOTTOM to the anchor button's bottom: the popup + grows upward, level with the row that opened it. */ + int final_x = ax + (int)bounds.size.width; + int final_y = ay + (int)bounds.size.height - sh; + + /* Horizontal flip against the monitor under the anchor button. */ + GdkDisplay *display = gtk_widget_get_display(win); + GListModel *monitors = gdk_display_get_monitors(display); + guint n = g_list_model_get_n_items(monitors); + for (guint i = 0; i < n; i++) { + GdkMonitor *m = (GdkMonitor *)g_list_model_get_item(monitors, i); + GdkRectangle geom; + gdk_monitor_get_geometry(m, &geom); + if (ax >= geom.x && ax < geom.x + geom.width && + ay >= geom.y && ay < geom.y + geom.height) { + if (final_x + sw > geom.x + geom.width) + final_x = ax - sw; /* flip to the left */ + g_object_unref(m); + break; + } + g_object_unref(m); + } + + x11_move_window(win, final_x, final_y); + gtk_window_present(GTK_WINDOW(win)); + + submenu_popups = g_list_prepend(submenu_popups, win); +} + +/* Build a vbox of GtkWidgets for the supplied items. Used for both the + top-level popup and each submenu popup. The submenu_open_data attached + to submenu buttons is freed when the button is destroyed. */ +static void on_button_destroy_free_data(GtkWidget *widget, gpointer user_data) { + (void)widget; + free(user_data); +} + +static GtkWidget *build_menu_box(xembed_menu_item *items, int count) { + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + for (int i = 0; i < count; i++) { + xembed_menu_item *mi = &items[i]; + + if (mi->is_separator) { + GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + gtk_widget_set_margin_top(sep, 2); + gtk_widget_set_margin_bottom(sep, 2); + gtk_box_append(GTK_BOX(vbox), sep); + continue; + } + + if (mi->is_check) { + GtkWidget *chk = gtk_check_button_new_with_label( + mi->label ? mi->label : ""); + gtk_check_button_set_active(GTK_CHECK_BUTTON(chk), mi->checked); + gtk_widget_set_sensitive(chk, mi->enabled); + g_signal_connect(chk, "toggled", + G_CALLBACK(on_check_toggled), + GINT_TO_POINTER(mi->id)); + gtk_box_append(GTK_BOX(vbox), chk); + continue; + } + + /* Plain button (leaf) or submenu opener. Show "Label ▸" for + submenu folders so users see they're nested. */ + const char *label_text = mi->label ? mi->label : ""; + char *display_label = NULL; + if (mi->child_count > 0 && mi->children) { + /* Compose "label ▸" (BLACK RIGHT-POINTING SMALL TRIANGLE). */ + size_t n = strlen(label_text) + 8; /* ascii + " ▸" + NUL */ + display_label = (char *)malloc(n); + snprintf(display_label, n, "%s \xE2\x96\xB8", label_text); + label_text = display_label; + } + + GtkWidget *btn = gtk_button_new_with_label(label_text); + gtk_widget_set_sensitive(btn, mi->enabled); + gtk_button_set_has_frame(GTK_BUTTON(btn), FALSE); + GtkWidget *lbl = gtk_button_get_child(GTK_BUTTON(btn)); + if (GTK_IS_LABEL(lbl)) + gtk_label_set_xalign(GTK_LABEL(lbl), 0.0); + + free(display_label); + + if (mi->child_count > 0 && mi->children) { + submenu_open_data *sd = + (submenu_open_data *)calloc(1, sizeof(submenu_open_data)); + sd->items = mi->children; + sd->count = mi->child_count; + sd->anchor = btn; + g_signal_connect(btn, "clicked", + G_CALLBACK(on_submenu_button_clicked), sd); + g_signal_connect(btn, "destroy", + G_CALLBACK(on_button_destroy_free_data), sd); + } else { + g_signal_connect(btn, "clicked", + G_CALLBACK(on_button_clicked), + GINT_TO_POINTER(mi->id)); + } + gtk_box_append(GTK_BOX(vbox), btn); + } + + return vbox; +} + +static gboolean popup_menu_idle(gpointer user_data) { + popup_data *pd = (popup_data *)user_data; + + /* Destroy old top-level (and orphan submenus) before rebuilding. */ + close_all_popups(); + if (popup_win) { + gtk_window_destroy(GTK_WINDOW(popup_win)); + popup_win = NULL; + } + + popup_win = gtk_window_new(); + gtk_window_set_decorated(GTK_WINDOW(popup_win), FALSE); + gtk_window_set_resizable(GTK_WINDOW(popup_win), FALSE); + + attach_outside_click_close(popup_win); + + GtkWidget *vbox = build_menu_box(pd->items, pd->count); + gtk_window_set_child(GTK_WINDOW(popup_win), vbox); + + gtk_widget_set_visible(popup_win, TRUE); + + /* Position the window above the click point (menu grows upward + from tray). Use measured preferred size — default_size is -1 + until set. */ + GtkRequisition req; + gtk_widget_get_preferred_size(popup_win, NULL, &req); + int win_w = req.width; + int win_h = req.height; + + int final_x = pd->x - win_w / 2; + int final_y = pd->y - win_h; + if (final_x < 0) final_x = 0; + if (final_y < 0) final_y = pd->y; /* fallback: below click */ + x11_move_window(popup_win, final_x, final_y); + + gtk_window_present(GTK_WINDOW(popup_win)); + + /* The vbox+children retain pointers into pd->items (via submenu + click handlers). free_popup_data() walks the array recursively + to release labels and children buffers — but we need to keep + the items alive while the popup is open. Defer the free until + the popup window is destroyed. */ + g_object_set_data_full(G_OBJECT(popup_win), "popup_data", pd, + (GDestroyNotify)free_popup_data); + return G_SOURCE_REMOVE; +} + +/* Recursively deep-copy a Go-supplied items array into freshly-allocated + C memory. Each label is strdup'd, each children array is calloc'd. */ +static xembed_menu_item *copy_items(xembed_menu_item *src, int count) { + if (count <= 0 || !src) return NULL; + xembed_menu_item *dst = + (xembed_menu_item *)calloc(count, sizeof(xembed_menu_item)); + for (int i = 0; i < count; i++) { + dst[i] = src[i]; + if (src[i].label) + dst[i].label = strdup(src[i].label); + if (src[i].child_count > 0 && src[i].children) { + dst[i].children = copy_items(src[i].children, src[i].child_count); + dst[i].child_count = src[i].child_count; + } else { + dst[i].children = NULL; + dst[i].child_count = 0; + } + } + return dst; +} + +void xembed_show_popup_menu(xembed_menu_item *items, int count, + xembed_menu_click_cb cb, int x, int y) { + (void)cb; + popup_data *pd = (popup_data *)calloc(1, sizeof(popup_data)); + pd->items = copy_items(items, count); + pd->count = count; + pd->x = x; + pd->y = y; + + g_idle_add(popup_menu_idle, pd); +} diff --git a/client/ui/xembed_tray_linux.h b/client/ui/xembed_tray_linux.h new file mode 100644 index 000000000..18a77c4c0 --- /dev/null +++ b/client/ui/xembed_tray_linux.h @@ -0,0 +1,81 @@ +#ifndef XEMBED_TRAY_H +#define XEMBED_TRAY_H + +#include + +// xembed_default_screen wraps the DefaultScreen macro for CGo. +static inline int xembed_default_screen(Display *dpy) { + return DefaultScreen(dpy); +} + +// xembed_install_error_handlers replaces Xlib's default protocol- and +// I/O-error handlers (which call exit()) with logging handlers, so an async +// X error from a tray race doesn't kill the UI process. Process-global and +// idempotent; safe to call after every XOpenDisplay. +void xembed_install_error_handlers(void); + +// xembed_find_tray returns the selection owner window for +// _NET_SYSTEM_TRAY_S{screen}, or 0 if no XEmbed tray manager exists. +Window xembed_find_tray(Display *dpy, int screen); + +// xembed_get_icon_size queries _NET_SYSTEM_TRAY_ICON_SIZE from the tray +// manager window. Returns the size in pixels, or 0 if not set. +int xembed_get_icon_size(Display *dpy, Window tray_mgr); + +// xembed_create_icon creates a tray icon window of the given size, +// sets _XEMBED_INFO, and returns the window ID. +// tray_mgr is the tray manager window; its _NET_SYSTEM_TRAY_VISUAL +// property is queried to obtain a 32-bit ARGB visual for transparency. +Window xembed_create_icon(Display *dpy, int screen, int size, Window tray_mgr); + +// xembed_dock sends _NET_SYSTEM_TRAY_OPCODE SYSTEM_TRAY_REQUEST_DOCK +// to the tray manager to embed our icon window. +int xembed_dock(Display *dpy, Window tray_mgr, Window icon_win); + +// xembed_draw_icon draws ARGB pixel data onto the icon window. +// data is in [A,R,G,B] byte order per pixel (SNI IconPixmap format). +// img_w, img_h are the source image dimensions. +// win_size is the target window dimension (square). +void xembed_draw_icon(Display *dpy, Window icon_win, int win_size, + const unsigned char *data, int img_w, int img_h); + +// xembed_destroy_icon destroys the icon window. +void xembed_destroy_icon(Display *dpy, Window icon_win); + +// xembed_poll_event processes pending X11 events. Returns: +// 0 = no actionable event +// 1 = left button press (out_x, out_y filled) +// 2 = right button press (out_x, out_y filled) +// 3 = expose (needs redraw) +// 4 = configure (resize; out_x=width, out_y=height) +// -1 = DestroyNotify on icon window (tray died) +int xembed_poll_event(Display *dpy, Window icon_win, + int *out_x, int *out_y); + +// Callback type for menu item clicks. Called with the item's dbusmenu ID. +typedef void (*xembed_menu_click_cb)(int id); + +// xembed_popup_menu builds and shows a GTK3 popup menu. +// items is an array of menu item descriptors, count is the number of items. +// cb is called (from the GTK main thread) when an item is clicked. +// x, y are root coordinates for positioning the popup. +// This must be called from the GTK main thread (use g_idle_add). + +typedef struct xembed_menu_item { + int id; // dbusmenu item ID + const char *label; // display label (NULL for separator) + int enabled; // whether the item is clickable + int is_check; // whether this is a checkbox item + int checked; // checkbox state (0 or 1) + int is_separator;// 1 if this is a separator + // children + child_count populate when this item is a submenu folder + // (dbusmenu's children-display=="submenu"). NULL/0 means leaf item. + struct xembed_menu_item *children; + int child_count; +} xembed_menu_item; + +// Schedule a GTK popup menu on the main thread. +void xembed_show_popup_menu(xembed_menu_item *items, int count, + xembed_menu_click_cb cb, int x, int y); + +#endif diff --git a/docs/io.netbird.client.plist b/docs/io.netbird.client.plist index f42b6b3d2..800ecead1 100644 --- a/docs/io.netbird.client.plist +++ b/docs/io.netbird.client.plist @@ -96,6 +96,10 @@ disableProfiles : hide the profile menu, reject profile CRUD. disableNetworks : hide the Networks / Exit Node menus, reject the related RPCs. + disableAdvancedView : hide the advanced-view section of the new + UI. Tristate at the daemon: set to true to + hide, false to explicitly show, omit the + key to let the UI apply its own default. disableMetricsCollection: opt out of anonymous usage telemetry. --> diff --git a/docs/netbird-macos.mobileconfig b/docs/netbird-macos.mobileconfig index 53453db5c..9bf616094 100644 --- a/docs/netbird-macos.mobileconfig +++ b/docs/netbird-macos.mobileconfig @@ -144,6 +144,8 @@ disableNetworks + disableAdvancedView + disableMetricsCollection --> diff --git a/docs/netbird-macos.sh b/docs/netbird-macos.sh index a2f5ff5e8..2777b407c 100644 --- a/docs/netbird-macos.sh +++ b/docs/netbird-macos.sh @@ -64,6 +64,7 @@ disableMetricsCollection="$NULL" disableUpdateSettings="$NULL" disableProfiles="$NULL" disableNetworks="$NULL" +disableAdvancedView="$NULL" # tristate at the daemon rosenpassEnabled="$NULL" rosenpassPermissive="$NULL" wireguardPort='51820' @@ -160,6 +161,7 @@ main() { is_set "$disableUpdateSettings" && emit_bool disableUpdateSettings "$disableUpdateSettings" is_set "$disableProfiles" && emit_bool disableProfiles "$disableProfiles" is_set "$disableNetworks" && emit_bool disableNetworks "$disableNetworks" + is_set "$disableAdvancedView" && emit_bool disableAdvancedView "$disableAdvancedView" is_set "$rosenpassEnabled" && emit_bool rosenpassEnabled "$rosenpassEnabled" is_set "$rosenpassPermissive" && emit_bool rosenpassPermissive "$rosenpassPermissive" is_set "$wireguardPort" && emit_int wireguardPort "$wireguardPort" diff --git a/docs/netbird-policy.reg b/docs/netbird-policy.reg index ba4402e50f956facd45f368b214ee6fbfc6dcf39..83d07c9b00404570e9aa8eac3b4ab71e4b149240 100644 GIT binary patch delta 40 tcmeC;zQnyjfLYv;A%&rgA(0`EA(Disable networks
    When enabled, the client UI/CLI cannot list, select or deselect NetBird networks (the corresponding daemon RPCs return Unavailable). Equivalent to --disable-networks. + Disable advanced view + When enabled, the client UI hides the advanced-view section of the new UI revision. Tristate at the daemon: 1 (enabled) hides the section; 0 (disabled) explicitly shows it; not configured leaves the UI's default behavior in place. MDM is the sole source — no equivalent CLI flag exists. + Disable metrics collection When enabled, the client does not collect or report local usage metrics. diff --git a/docs/netbird.admx b/docs/netbird.admx index 2f7645d63..1a53a3b64 100644 --- a/docs/netbird.admx +++ b/docs/netbird.admx @@ -207,6 +207,18 @@ + + + + + + +

    ;$Y7|`GmdsUGKN|{m7r8 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 fvks?6@<`I3J33Bf@Z{MP-A)! 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�@c$i|I-1BZcDq4#G3!BYm%Qfh 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
    >I4eNfT#ckCRoHiLZReBWlTv9 z*Q;l#+dfr@V*8$9h^2{SI|nl--t|-N1RIfWrT`F8w!GJPwPl<>*;~J6XOv^i1C450zt;I;Z z0A=_l(-CvhZo#uA zACfKNMCI}%94;B|m60(Ok*-N_Mu`2}Ld#b{u<9}{hFmBIXLAih%VcNJ)|5u3Ok@H9 zks(8%G6y$I880&VO7K8PCaMsuWiPZyC(xcBWXsdP7Gno~+yVuGHvAe6$A0LT)DsrJ z;4hwTNhy^LMaUtRi?4;(`M16zVA{h%G8f@I>etXSL@WD`?P4=RMtg0#-C07km^*V<)ThQ zcS?19#%2+pGgj$1j%jX+`1>jG|Ej7<@tf&9oYPRuX&zI$y1Gv1(=pBQd)+vthy6Ll zSEpy2DwT7Q`k~Nw5A&R!KeW8$AKwtqFvs^b z%eehAb*EEmtCY$jZX^DDh<}Rs{YAr_PpR+DX}eyhV|;eASfnxTqwl9Q4rBa&k(#EB zf5&%?aUcCD9d})N_GA@*A0Jqz`1_Q`_~0q-H$FI?Yfk4_mnOcaUezgeL)>qb`ZT9? z9sfHXobQdLiRYWsDgJ5eIo7^PLwqOip`Q6(?r)A~nNl6U7atw>7|%Wqsp5C%GR0d; zZQSD!@9Ap2NpsxC`FxI_8TEUy%BgA>af3q|Pv=y`vvf1}FsGYUlZLU2-x*U=vi|W5 z@ww}GAl_YBHtE&NeY&}7;=RuNO8i+F>sqBIZlH+I@A@$vGst=t(I+CuX)Zg9gnduhAu5)1I*PTJskVgoyT~t zIxUJ=v-r+^y!$zxb+cN=Z^U=Rvlj8WDenJpj%SHASud;jIrc@ogEHxTu#O9EE`IlR zAMciT5PNvj#_o*0*~A`dVvUFRA1Bs$F0^*6`DU|9Rs8vRihJz(bhTjb#&^fhJ-Ldt zjqg0ff5(TUQ+zL*?-^?rpFhTbALBWe3pQXKXDNPf=wsi;{vG2T$GTo`nzZYtbckOq zW8ZLA`12~(dSW}oGc01W6me$w{AKLXZW{Fbytn&U=Q{q)zCFkN@n^Ah^eoLX*71Iq zZnlf~Y0gEV_f(dJ&b8O3N~gZi+JE!|i+E*IdU-d*zF-f=d5(7!J0tEn-fQfyk-g0x zjdQ$;dpX6%ILDv&aUZPxI-YfoKRfX(iT%>1{cacQ(WGUZvp)X(bd2|0$4QO%$|9fQ zyLP7`t>QV?%Q_w6zx$$2%W@9867RKa(>d087}4N+wpgyxBEGkX|K?|guu+5VE&e;66Z>^@y-KJ3KK5U$ zeR%3(U1OEIxUaa&cz+dJI+gMBbNv2(9@2KPiqjs?6weZtSMOpO`(zP6i0K)y13k7R zY;*j<{qC%H*I)x;y|l06nN~ILu1}}fFC(kRCTHC^Pht14#ZBCQ%lqN)2k$f9Bj=)y zwLXT;K7~CQvn`zqY(V@>{9GUR$60*3itlb4+4XsfvmEaVo5ge1@eZ+DW#RXDir>+m zTC}k)U0R22(s@qYT6~7qGHepoKJ3fYW+DGn{o`MM_4bVMh=uFo4;FEAY=%R;$!4{Z zA!-&&{UXLrn>U`9&Bt?NjN7>B1x7w@?i3#$dna}AvMTIXxUVvdH-?XeIrV2P=*&W} zY49lXEQ8+GHe_tRQ42A}_k`1nL(-q^T#}3=FSXb2#V^R=Vr<%FqXUd17^g`_YuWm{ zCpOT;_Y_)i7P}9}a*XFJ+t|qX*!bd#mm8m1$7k0pUVO%R#K70-Nf{ey(8J3JZDO&< z!7rG5;b8LftJu&uk3|^PcxU10!`Q_AEHVCZ?>Gt$?7*5#Hpn>sI=-6&vk3>n+8*NI z%Ir>qn`rIx82kxD}$Dr~&IK6YM503rm%{tb-)H~)7V4$aLi1Z8?qS$k1 zIZ7OGd^*N1`Te$h4+|Lg6}wRfhrQ6p{VwC(G_lSLHeu!anI;_0sQ+Gt6Xl)t=Rrnk z6-K$4&S}ZJ#W2R_PSYqy7)z!-d9{w;i9I~V=VPSWSU=t=Mwb1^GvdU!R}90l#F@q3 z3TNEL=iG#&-nKmNq&-0ZQ#GX=*%145mQx*cZU`jX_>YNwKF|7ZK0f?MtTo0sz9II+ zd5UM(GZk9v6KjN#!ZYfmPC6UQI6D);V*GL$>w=L!hCRRu4eU|wKh~A+;T+=}hd4jm zFj5$13>)`Rv8!UeV+q1wPBO$@oKHFLnR7VHhwvUR_j5eQ`nHAMAI@eSYY?wjfhPCC zNyfP0D3-Cl%M}51spn$9uVb@T@%d{uT>JsXj$nt~hf~GL#skD>gnbA*vJT@HHyb}X z%IV=)x&JQS_03f{roy&oi1)ON`^P2_0Jrg(`Z?B*FRnO0qkeV{M?{dw zGRhe=vESNwmpF?d))nVg#{sPgQg!|d*$(cr^uHARvDVwFjUeTu=PV*{VNWkHQaRAZ zmSAJ=kG+DqVI*K8Rq)0BQJ{hyD_E0J-`T_-$Dvh&ZTO)3D0AR6I|(SgZ(i*Vx^J$1 z1Rw39(ckla*q0;01r{dOiCf#m_we4aE9c&OiU5q~#5VBmuqA}@huBN&u)(WEoc}mu z1iL)`VZatlN;=rTjSR{p`&ND1M;33B)g(Ps5s`2u@6RDF*rY6C{4!s<-Z8g~7<4bu30j0z*8=vh_=xm4$e5b%;20{D};< zqbVYQE({V*bjd+Zv2dmAuA)bb{lvo(?Zxijte0{ob3`k%{h~7lPPUw>qiPHZu4ahC zeLVFtSe&>v3#vnTkS@ zWgl;ody|pY2JxEk2-mAc-0vpt8|N15S=BOz7&M$zqjYi73*5v395}}DLf?3{k<*QD ziFFy}$T?UU~n4K_&u1<4M3QPFgKF7Ig-ksybh zfQHDe??$h2QRiI|#y39yDl#gJ&^dmYY=wivJDclPnIW07qUza3qrzy%pS^fLvd59< zne}tZ3~~@9SCNepYtA!b7|W^CU$95`9h|Z~j0m`J(u!{}q(t;&0rBru z?5nbH#5@o{#h+ir@8Q7NyW9uC7fX30Ya+noKH|REo1ix5SZ{(4&SFs*r63!}+2iyM zXWNc?Jj>u=4DoVuPy~{S%7$#n0%ai!-mH~@^h2-p86CAbsOGy;Qs&Qnfus;rDM%R{ zNRT~qPyFtLQ;4(QkN(+H*s~=8ogDb2y-g6rMX*dDwhVNKeRxe)M8FsKic_gezYisP z$@@n(&ACJlI*qho;7JCSI5I+$(Sg8N77Bo`n?<@i?Uhh)_6ZJoeVecyZ8)g?XnQmW zb4uw+1VTwp-DpXXoeu#I&%?8BH?0sJ>`EJJ5~m{GJy}?plZip^lXLwL??o9e{(uw% zn;glEg8$LyPDnvr==(RZS9oVT>}b2y{salS*m?!nvX5ay;ykWn&v*BS$h7lWO0_a& zY*$^B`Df#~3S0hbzNUVg=!-}ar+~L^BRa>JjIlAWc)Y+yM!L|c-q9)aj7dn@apU_VX$ogQ=-}RkbDT)2V*_F^mNKw&b~v$NYeRBmxbnYo zPkUuKrHoaTH#0%VE}*w6HeMeFWX9$wA# zwXxps4>Ar!qGK2f0vsUBJ&~l0|D?@N_JR|U0jLYXHaN>N5P_>WG+~7UBC7-$9|8)C z-%g8Fqy0@>_~z9rjN~!aEdrca3!t9!WH^kECAwb3V&a%zL@Hj!z67S)pPW?!E#X*z zdNFcy>><|s2xNA(cC;R6pqIf-j{eJ(_eq?mQ>V4>WyS^>c7e#TZVv&G9(re-7$Xh{ zj;#uZxsCM#(g#W%b9RIsZJJUBpLiH3XA?hn9sBe!MG#el(*W|Da0-r+n+OQrxNg() z>t*b-cn8OWGQ{ON;J*kEvCpglP&)5>L$FvlLt2GV_GeDMoML!qDyZT&h6;5kYh zY>iW_)5{3<;(!JW)C$TRr(SM+nw`l3ck(H#2sr!LpBP*PHVyY?NV}~Jg-!9Z`v@l9 ze>f^~CrEAWqtiati=cyj$T`3e#!*ez0h_k*-^ViuK_CT(PR8%%dZX`Qf0kvXceRXz zX|y7s%ND~O`#ts%@ANzy(ks}cJY)RMlW+hd&}u-MRea7KR3TnUhnovq4 z>Ttlo_BhI>k`1Qpv*rspC!ik@1XbVlvE|!1ByJdZ;S`z25XSUAY7#9NuncWdO-mj; zx!wXghEu}m7eyStg7o+t2Ox}pIFmL$>oAXd7UzRy5j78&jtN45jv{GKOz4+Z@)Vqj? zz_b-v3ql*Uh`57FXP}p0ND^JgYkzoXC0a_Dss>% z8TO>UhSQA)xb1+0s2`Q-BY|&kNu4nWB`xv-1E!U;Eix1Yln@7SAHVbv zrxoYZ)Q%?ky=m$NSrB#d&ms&AwUCBjBP@=f-gR`Vt)|r9#5kJ4WL2{CaVujL}tT2;PbRs<2$e8y9kbka3riJb+b0^`*0Wq zXJ3(pE7}S%9&WAYXWJE$)%1RmXA?jCTEv)a7v#c6*r#ku7l|PFt zl;0&=;=YK<7rCxSW(W-W>WJOqd$Da%*NppU*#p?1*qiG(SH~!Q^bu?j`0UA)VoUJ+ z)Nsqvun>VL@TFkS_-{eHu~yHX+{pe7u{PK7Je#^vfZQv~Z58|-rcSlTuX*|`YNiJn z|30}Cg6%8f*$CXa!H$+a{xrUO8~cJifg_kyk1}+xKs)SJqio6bH%btTaC!<&sQ2S6 zbT8aH(4rxIJF++vQ8xuXRdSIsl2l;tBY224jn$2MaFIUvWF!6nnJEYi>&+T0DLp9* ziv%UgQSZFC5C7Aor?J-r$;ZA{9W3sPQox+c1M7IlTQc|XPt~`7uzVY@NLGT!#-p() z+b~Smn{7P&QJV}5yqGck6uTQmOvR??be%8@(*xGK>M6i3pG4Jzqi;^WN=Ahk~qtA}93&VYfsm;#BazYi;Hk z_=>{&d2|t<+SzP)h5IK0I*~;crJi$lHra!hwu?=Aq9z6mfI*@L5l(E^4T?H$aa8f0 zOJ!{?ykeZ$xaAs_!4FTG4R)$ElNfg*c@znLUWW0EE4eNTl$!YqeAVKxS z*%3U>aT_(7WjUm0;ZPw(Jc;a;6dlZdv228i3)hZC;oTliWIm(dio1xaIS9i0Q<>|G zIfJSF?i)qzqtgVzNWZ4|*eK@dPJU@^r5&9z6+sKN51eJA#kv&A@TNbgm<|qcAVxMfUR3JXa?*a?6 z&vAA|7!F|V^|ICBq3#7qN#(i>-BB*u9VTT%WC_$VKr7g1M2-*rqys+^u*B~y*VKiY zShH|6>xee@y%2@t;9Av!mmP3fQ982UA7>T$Rf{BYZtZNwqEda2NFF2C4{`2hlW=%% zWLp^iH=aLLHtw=Xp`e0wl;c-zE+XHh>t**R*AmFMFyg#Jh*Jb1lo3S4SwiHf^>;XT z92bUh!pIbrYKKJn+c@Lf#=++zoW)@_G|YQEiN6thAZ(6m9brqJZfZGSXkWm=eGbmZ zzzNj4FvK`h?(b%^inU7V?iK<{qm1?;f`Ep1-rD&BQS%YotEHg#4UlNquvY;~0e>kl z2AzrLp{6-!5*N9ewWQE_UI~tirDwz);6ry`INTUL~2iED+h{$6ETN6Yn+cGdtf%fY?y6xuwjRI zPVOt|T`rWZw}M(h? zjs<@|-UTiZ8x3rEAA4;dYsEbri1_1vS1LV`3;=movN_eS`{?3rMpuIRZ0b8m!o77>vyBRT<6;D#C%;wKsH1x&5d^?@Se?&sJ5 zG71<~3?Y#ci`bpwMoHsq!7CWB7Um*_=8V5%NKbt)NDsJZRK3ouHpM#_&P6Ln%jO4q zBAVuT>trIp2cBPx0ZD`hBtpaibft|Am=g=RyYJJpo23pu_$>#FXjHW;;H%i&_hImk z)2wHPA*qNS+XIBTT`L3R_p8#xmNpFHW?N+^QV2)yB2u0PW68=o8yAb&_?zv*nINPR zz0`=FAuncl5hHfO=n9J-SY=a;f!;O=iD^a9aQo^Xoe&T0wg>$L( zBLFxKlj=b_0zf@s0Gim0I4h#9MLp}#ve5-I0yjlu<F_RHE5!nH;Q4uH zDc;KkuueSFCM-70CDxYjBD%i0*`^bjz-%U-j){;KL+7et@orm3ZLb)ovgn@?6cU)_09Hdd=fQnS?Y$c{klLt$VU3ed@C|_u~lc)U|SG}9S(UNMq`w5u7IYAc!AN# z{#yUHh)rYy@iQ1LYK1qT20~-ZMx=(M8sy2aBLpGN=Bsefur0|(RN^1(QBO0$+i{8Sd+CXx|kIRtOHVCgnig@{>g$O(|8dMa1VMg znSopujz)l$b-u{HA=-o^8%y&BsQIxzCnWPX)XCX~g$PzgC9{I6OpC>dcC%S#(3f;T3;*mSDs3g_ z3WJZ-7Cbj@B~c<`^Ac67J%B7uMb|>~)D~t=n#3j}m8SNj^t>?DwaV-vR?2b0s5HG) z`abD-6!}bZCPK`LT7bp`Bb-N7HC5Cv94YaMM5wI)I2LC2k&0l57mbS3Q>L&tV)qnE zw?$d5%8ZGRD!gwNr3@t-6oVu9K+tW{p;|}grM3rUcu-yoIl4xrj!Ey331&f`Mn3+SaPl$jp*g2BH z<8#zrP&i8~3zLu3+A#Rv>{k!iIM=n{B4^EVj&tF5fq32;>Np2G3_{rGbblOma@ZdV&dQ_+lgGGZq?^`E zNA7VhCKETBQgtIO3%?}vvJi9&sC4SwUQv!?CPb8ac7A83rGlDHeBRw|5W+EMB92Kh zUgvQ#SxKJ^rf=gP(t zdlL?i+L=ler8AT=N9ih4jNXgn;ndL&XGhf70l&9FdudVny{gk7zVBHCes!D)B6||4 zEm@45>>!c>=n#RW;p-#^<$XFSP-qlT*hV#?>{cS@jQylD!*@-CGZ*$@N@YHGow5Y> z6)@!~kfZBGZNd}m>*P$^b)(U&SXx;M)*{d#>ZP#0M}lu7QPiGS1Yp!#CzlaGLbUh0 z%%G0%rb>B|(;%yEa!Zbb;&mh!AT-!$f*ljW41tq@5|r?{pcUh6hIXemfJ$v3b>(&m z_NgpdY4)e=x*+mGd-t3JbMD#Y47bhjv)7R^!daMSvzBm}WV9qZ1Q905C6`Gc{IzCm zSHJ!jtlpNxsod8BSAGWHt{WXR&OtAVxv8uo?Zkf*(Xt`=0^kOggTLdjvhjF`(x*v> zznD|*14{a0QXky_SyyZhQb;`jn~naTx)4-C##cwwN#Rv=+R2BH=%f)GGAZl800YYa z5iKG$-bX|PT!O=xE3>Fzeu>Rj*a~jqbGI;#C}^rCHb;{-h zPK>A*0AMth%$k!W;WVgSP;gbo3j>R|L=lsFeJG@sVksH}A!6cp$-GQ?Z}pDULI8!e zt;N!VG1??Eu8QnvF_?wky(4?Fo44M2GY;XsqPL3Q6o@;vM2hD_q$jC zCz-y`)JcD>W0QY&UO$Ke^vJ&+M7)YR-hEcbEwFr4oy-N%PZ~$ zL5)t2R1lv;|5dw1rc$dn2j^CqNj1xWCDl>qvXMvNXLk?ItVnSb$yYA6cY=CK5g_<` z4j5}pro``^w6>Fs5?DQZb{%WAP!QDXS@)5N&@<=2r4oBmfgImGj0!T?V(v3DEY~(- zz3A+*w$${-$@_&&^m;=rV6Q$c95*y@nJjveAnpvkvy{j@L}jNZiC|IWq%oP}JIkQ+ za~KeeYFWCkOnpwJnYoKLGfrm*U1i~{guO#V-^DrK-Jca4lFee^*~inA1BLi}&;YJrt1H)!r)b$iuGF9(8+BWLInVke+k@5-=>8I{S)DaIq+aMyXd6_>OEOY~!{r1q)6hERj|E znQ^ooXQOnGA#XIArN`MFm4HN$8tbK?6|^loEzX)&8I)vn0KhZTnxiu{n%#MTfTX*f z+(ttGDf=1-mHyk_^0gf(UAl za%M13xnaSCmj*bZlvWLWDvS@VVep_7@B10? zvXa@3Nvx9D&IXr9(T!AwBA^$y6pTfL(dHr|ba)E$V#*hJbIwKXpR!>b)xJw&8At7SdodrTu9wEm>#f>6k2jCPty2pqs+fGDbrsg$F|2$m^j z*w5+sg&8LbDKOQXsn?YW6Koj4mGN!_2@HiDjGA~??aCpjYS@$n^gY0O*_a6tWe-(8Ct^;L1rH`( zsRS`Ns>bLOcn!*^8f7s+i;zN}ZdyUW>>)MLs%I`K08qV)9Tv68Fk?}xI?Ey8(8S`5 z?;-XCk=UJG*Q?{O(y;sL1eHIa&Dz1Y96_%8>3`NnSul=dg2QZkQ5B}Gt5LQxl0Y!r z(F(wsDqNdq4~td~T%8-imAjW;YX3l-U>o+19>_V@EJxN!eD&HaUPvBMU$dw~)YBF& z4>+cB?{M!_a_kZK_DS!L|Gg-km62$$S+!3qL(Y9>AKHfYLVaYr_!OK3 z4aiNE2`RmpkTZlL-YGVNV4c0vJ3t&&KXbxzfnXiZZK+D&*~%m+sK^_;6<4n`v3@f zn%xtF@m5qdUmz9tZ_pbi24rm*Y#~Ci>W;vLx8U@`9Ngo&m z-9CT+LeG$!cyj}g2qeHDe1{4gZYFjbp0Y8?uw+?eC+~ZLWFm6=MJWX`1&5vHPTukC z=0T$O)94qX44||kiK`gy!QfpSqn0BE8YCK;v4I>)?<0(X>YJj@!d|o^LTQD@)Hxd@ z*`UH%99D+b3uVRgl!*`SzhXau)hpu55V0XK^@9s$gTV$&iL<4C!oe2|F4BxMFR}xm z-?Ms#W;t{P2!*NY>RF?Gf?(l1C_)CF5Z+#sZ8a(}xM9MEQP2pE@=To}ps~}Ss2c;c zTo|j@?xKH<@3763GdjrdWtb1Ako&p#zd?s5rR(d3YExv)%19?0S_LjrAg}}F&scxF z)A3~HBRyqe@zwde34ds0o^%T3B&mI=u3I?r#yMbQI8#&dKGIoN6S!;N{28TyQOXf! z85>J9nqYxirF3#^JG2UK6W$Xv?;DREFZs|j1gm)e=4 zVEd|4AjUpp?K@Q$YMpc5JMXTpTsKzpxxTyCIU+FLAC9uqN{liD*<`p-|5TY-m~D^V zMYc64<8V%@$~NHBqIk&5bjzJBwzd1ihYD*A%e5ALIwGd2r)T&WgNDw0>X^k8Pv z5|&kOX_S?4=2=m5pN(ysvk}(}-1pR0UQ8U&|grAZi-R1g{&5M^fb;MAX}1fl^Xg3>)YWmXI*vup~KWF6MY22l{>oSW_FQK%Pj zNX|CQwUq%B&=x625tcV1vmjECIzns)3YkElMd|E7hWb+9qAnJOj(#XeA;oJ*(6T?& zFR1xk;Ind`b5>$Fh0TPaltfiwr!QEp^riyJQnp)7CYn&kSyY0|M5@RKt5Wn<>+>NF z&@*;YEx|BauD1Dbmy@6_^@L^Paz!(vHmqsk(|l=stegc;ephYnjj4W`$_Km5&5>@? zv|$jOv$QdoMFO%cm4cfr7o(sAjGv-@ZFLPrwHLy~?95jOVY;kIA$&gjd)0QdzRCMY zun6nk+5qsO=Mzk-g_*RyDdKaEB#9XLN5mzUx`ZCw-5;ekLp_FPQBdQuj()0bbASd! z0_|dNpob(QNSEL)=!YPO$p zPmzLqAbHwgT#KsZJj`~zG?^YwqkBi2&c}91TW3fRJ#6 z%Q_#HWT&mx;KG@%BJ0vlO{9^r&yF1Ux=?gahQj~(j7_eaXQC792nl0Z8tyY;%g)a& z1Fg^?^{_z@pnjO4Q@;_{aZ_$^#(@C^gE%OlkLMLTZ_3Iom2DW1Y?TeZ+H#6PbuTq= z8&TAz;NJvHs*#Fu=r$U9-`LculU? zvP#C7i4%f78$-B!N1tn!BF2qQ0I(Sib@pVvtV>X~5GvBDHAj7D)MyC?rulUi+2nEz z=b_1}8?HU)`ZG>{bbzPeqbhTwx|NvpbG_Ak3@**DZ4E&s`N%oj6S79$6_oqxJ3qF3 zyB9q+91qQQrBh?-MQjpvX|sHV2>?V%-H`SCGI>CTO--PMoY*YGSm+SZpU5HOg`JPR z3W-fD2Hg=vSaOEKFT9DRHc=Wg=R0l!tDn%Z!z)*9u8UPF`hndmn!#z*ei1w^@=yij zi89BSFn=g;6H$(4<>0J3sH#W=#X*aTZ39TDWS*_YAT~^0<-W5* zS(OeE)u>FSDC(s*W_GrVFl1eJ7#IhPfEb#cqp?cZ^Q6N;?}X^>l_MtA7Gf<|l~pTv zZgka%PARymrz^j7Vd}sflY8>Ozo8`)d|-^^IAC}l4X-(Kw%d@54{%!RtiZ6Ap-QuR zYy>ZXi=$eMX=`1%*$zZS`pL!=6$K)4sK~%e=iU@;K?y*8ncQPoDi?TLHzooBN*N{B zSF|3bwf}gZ1bfs?50cy(-PbdkY+$6mGwMS`qM|RgK9;$evYZ(K-D({NeYq5|2~vU>l%;#kDg{VJ4yGJj(6hoB_|TJ8 zDtL#$&P`TXpeeU2rU@Z7AB|qVI1A74=Tf!*q~$Y_ev`P-A5-iGP}`tl*6_9 z3@8eOhe{2$VH^&F-tj@QOAfN63Y81_1hDkc)j-G*_3pwNK{R0P%EG-j%7n=(5HgtE zyuJ%W;t9{3;re%kp)<=#c5+LYhj~n3b&o^{Uy|D2nVI8D4KaR%}%U`O{RJS+^=} z_r}rG^g=`)f8P!gN9@O?&dQ|Vv#lN6x_xO<3y*ghQs1EcgFr+iraAZ7mE~)8 zXe!*q3>%ot^TGitFR-5y71u=xq}A9&?Xzx;93&H&#<_PQ0?Nd`&5~~*Fe0av)__#A zer;*Bp>RE&m4Fzcpb-@3*M6vZyUM9xnX`;aKQ@U}fTx!tLZ&dvsT3}pR$7>RASX8l z*M&8|U{`I!pk1$;9IsCjSuHMledj?hU@7LJ6%lCCqqas3LTnbI)4+F6XRB5WkN_Y? z#EsKL%F8~U%@Sf`64jk^u@ytL&Ks7f&k*mZO>b&%(gqQPsrsH_5}b0WVyqVuV@B$q zawE1Nx@0LOMfGHkVs*KYD25x-MPaZX0g>Al!6do^O5mh^!_aM zRg03!0^Ohm2cZDc0hIsIQaEHQ)Dw4cPsh&9lymOO0FuI(VYJkk2ppwoyO4pzc{atM z$Ti+VIQv*5GLqU9U>yt=6POR<6-MmU?Ow(JvckX3y4W@ z@aq-EypDTJ8Rjk3Xbo(IL!yR4Jr(Dw($tK8;!@w~%)*EJ;pB$urS4^yZmyR)oMwr- ze@eBWc!v;@uC6%P=iH2ZwxUO2nJSo-^aqxO>&Vn}F}y{)3g{5l? z+vJQHh)7Bo3SOsHLhl+hDO5kLXYVgx|C<>eW~S}k-I>cv)pro@+Ln!p$O?z0Skmb&6zQL7jn zFur=|g~F7oo4aZD0_je(gB;$CvIMfW2vjiaw^1w;k)<**hr0xZ+`%R!=!|!{-85>n zr)FN~EM&;Y5E&`3qG^_)y#Tk4V`wz==?kwa@ zopCHo5)wKt?ZaBO6(ZYGWeLA;Aqkn>A$dO+I@3VgLuJAe0goiZq}Wqreb-Wm1CC~o zZfe`)#Ks^fXSc`*M9NNVlt6?7(pu185_48jdg(w|*|_S|k%*Lj4oVWJQ!E6PR_Q+0 zUfDW(R=w8lIU{*OguVK;A8FoxUCYRk!9EU46*s%vAN4_cNFXN!SOeUVsYRVgm9Vu_yyw<|*A=Ez^Bx5mT{UREwCE?GcA9SMGVCS@SlE z>5kInzEyj>5DgqP&w?q`;)+uEdZo`@x8Bec8>^gt-^FGxqaaAX&s#UE)E)O}PnK3V z^BdH+F{_}B@z`e?IjQj+jtFKH6j(_+)wS4h^r=bwGhw8c;Y3?0!qc=2^orY;cHnH; zl3C25BE507c3%U{$nidDc;qu1(e0~D?Y4|m&wGg{eB;Sd4y03WhM8_Dvps=jsFMNn zXaj*4AHsNCuN1X&h@hZ~9hTg{UCo_pOZ54k1UFRa`IUu@r=rSGk{u$Oi3t8GKCd5+ z=_=On{(h7ZnYDa2sB^i{AuNk5Y%8tFIlb}pI^N|mB4>y#WwuwNvZUp*6r6k(QN|e_ z|HVPu=-s=xPc!5d0aa;M6YvSebTZ6QV#)E&j*}eTg{g!9GwUqj+PPEckyN*=5ypTPg8G#7u9y-J05vljN;S0thLcH6^$dK%z z#e%H*jHYDl-}he~!$7&83fU-}IIvff42uGVQp>>T?6csl6|XE! z)!fH!p+=;Jda38H6@?q(pVUyyzdK1wku6%{j7*>r96q?jX?P!0?%c9)cUG&qi$I`8xa6u(vFA+ac6J6vC^|0a>K3lCMz@a)HBzr&crX% z(y)*H&sk+U0zt;a72CY2eWvkZga>t1ZtB;wL6_54ar z+1=#cOok8RDi_%asa`cku#t5chTIt3PV|!nY55cl#t^|r6s;{v95GbU+lu@$G*DWtP~tg!&y>IZJRSxzO#pl zT+5M)wj1B|>Y=kl7LrL35FDiAS*ytxmL<;Zq^Q$TB=EcM!cji^lg513hY1L@8Qg4Y zWnD$R(z#LO8jE?4QNnRS0-7zzd)N(HCye|B!c)IgmX*>OTM%v6%HpodQAU&2heCL_ zdj-?v@i$@%#shDzuNB;pnOqb#8bL&}_R0#TN8#Eu&COKHKv>EgFiFsMbhGhXn)Jb* zd?NzGFWh$FG*(HyvxDDHSu-Ux?XUsP04y4I~jD&{We>&DjPDoMEYXRdz(Y3fkKGoM;ey0p-^Vix(gnHmdJy zlG4m@IvVy?BGM?q=tZ{pw*SRD^Jc-#v96e?{^@kON7CUOO1ks3q+g zD+TF>wbd9b*KtJ#&%yxBWYrf$5yAw=HtVJl+qTyGFe!t&ARqqiKAQF}l21((gk=5}Nfic@#<^>U=NL(Pov6 zcV0bc?hZ)*>F{xXJBPT@>I&GeB=Yy=^?Nu z=JK3GGD(8WSad;*IPTW6{$MK*5!T?nkU9c)SQZxkU4?VHi}UvRShKOVl*#SG=usb9 zv!v`&^^AqbM-XulA&%!m7}GGWG)xZe`FQL3Q?ZUja>>y+8}17EoVc(19cln!VB$SU zre>0mktL}1oi_Xj-h8U>*%M%J5Ke=l&^P0=4-v4ij=R$&C5Tn+#=?HA;y%Ohuei5P z(cU7rD<}&RbrmVAQKKk`*l<=~2-rosWwU$!h$1PZ6Rirrj^DdRoVntl9M!YsxXvjjXi0#m-ax4M6P|8Xd@@`-1kDRQgT-s!@5{P~JZ6Xy{3vse|P?CSXwr!R)at9|Ez$cR(-(pAf)O z#C$ztO=2&-zw6`x`8~CKs5uv@szI>!M?Z)IKg$tNn+5WbplWp)K@-q(z6Jwh@t>u*V0!u_eP9c(yYI7jvW&k6_CfCyuXe`38u7!X|WVdv!ZI)AaOUmv3thy}O2=l5|t%HbR zGy_s|t!B@~f|W<_UR&fbM70%%R#{q1w58181XUQNh*AjEVZ8*`N)4$L+Vmu#l6p9~ z9f=7Hkr}}Ym=T7pEbT;axm{HG{addPVF&6=*mW|qvs7zL(?6>YT&6c7XclQ_vUqT5 zmRFv(;S!RhxilKZLK6!q*hHXMm-_Be$~uiQZ80<<6veYKh6Z7EI1ha!5U7!`c7+g{ zQNap&oBQKQfsPz*8kICjWLFVyZmR30${QPeO+k7YQO8ET5g6f%YBSB2=zEccQT$cS zJQn+dH`~~pqj=y4hK)^@9nz&TkyYWl*j!hwNgmJ9sw+%`*4(SuBz*sv#Y9h830}qf z+7T2O{5ib&XzE%n=vDH_*iFq7i-WS-Y!!7JfFXui-S`W0Ha$F0%*jZpN&h4A0B&Lf zPMV!%l>_kdHAW&l{;R{JaR9KzuF(Bcv_mgSY~Fr{G zA~V$HEwv9KsFIKPAv&Coi?1uj^HV_0h zo7Q6*(EW?|$vRYdZj-eZ9Tl@SnoTClGP!X({uAYVY97$iw=@s}2UJ#u-lP1?=mRo~ z)^!2VtZ}$%ux+H@21JPi0b<_X+@#OFcTV5%p=+)C7hj$g9AK~nkt!>6fsg3x;nhw~ zvSu{KlxG8}53Z1|Apa=3rMWxS|JgI~zs;V+5Nr?{t^yjK)YC+U)kd8SWB=k5&lT@* zlA1wdxv`Y8jvmI`Z5~8-)H7bbzmt*IKu;p!#=~*g1AGoAUp#QKAq3yH2Po8Y!9?=4vMS#MQU_jYd7;d!$ zK=*E~?(j19C)p8%fI1URkYXz4OoyWuLo*RIs&HJ--`u8mU!A0_=wNZyx>75L;3U)2t<6UR83b{MpmFD4{ZI7{K zy-EyTc(N6;zGqK1>HXVdICRS{iJU=+C*GITj({LzFRvF!b@i_UpD|}o*bmA+(r?6R z#-6(m2)xWl+UaQ4=Qz3{T})}5aOGKR*w$mz&md=@cPpigI2&u}c(!hz0AYe0C5Us- zNSWtv+$f-$@@zuQUu!Jbt~D(^q2fM1tNL|sRdQU?@*DhONb@F?lci@hsvF#_Ez{zO zq{(iGj@WcsXc3TR9Spb0&3XJup_`F9!8GPx5)Ztq)eti~#mxOYrl4OGwF`A&W4J_1 zC(0pe$@7^~;U1z3AAS~-#rb zPpbM5FM7h66jlzvnDD4L-6q!~n3Xd!e@QS{FYPeq64et~p8L9XgPR&DX2Ysdqu|2C zjB1`UG-oG$(h$#D!}Gy!5RJoIvv zGZ8@_<%P*tnqFO3I!rRkvlZLtG3!{49z3W;q9y^zft@olIFb`)?HVvXaKC z_W;J{VQ(l8r)njSq=UlnO*KOFOtP(HtVAx!d(XoM$E*~eMEdE*%Z#Ey697&?vA>zw zKV*rmq_Sq!A2m=?$|Oae`IKao`TbL_xv13yW@zq{)XcZSH7&$tIoznVyQLu0hLv^A zVpn45poXKIkE35Y#(3RgKpV%Dl|gW>HyhW4*k9{#iVSBg1kKHITvFiaX!W zce~!wXQlhZ%1mJvVl-#Q?&N;1?Hb0QNgJwbQMWx$zjQ8QgV20r+%~RLal+o25!l_<4kQ!!b ziM4DoX*KVc-k((-ZpcG2OgBlvf~nLId>=c+1!Dvy&cu@7bG0M@ z_2p#1EdW!V@#pe_YUuRhi9cXS-SfJp*E)U-nJpkT#RJM#%-*N6!C4wlEX3R>-p^LU zNVDxT0U+5V>qDYp(wJLRQtpARw2syqJ!>3`TLP%?Ym!2;PvDdi=<*&wVhB2iOmqSv zD3aF_L}~6l0*WJ4m5tsjXXOGBo<=_h0X{)oZkkXAEA@{oLN3t^_3dVI%gMD+v|GjC z{`J3L`?jW~magwmYmnn9J;TLK1*WMla$|yq+Hv4v?kz3zPvnWx`h=k6XdOF^!Jf?e zSxSJi)M)M-dNacDUB_W$Y&U_vGWW@|XN2+czEO+F3%|%sgwju(GuA8#r<7%3d>FU~ zmA1-UFdjW=c1!apMs@OhD&W9IqytZZ)jgN?^i!tPj#A8Uu~LH-2a_X!i+npCGlDa) zs-xA}aXLU7b89+s2YEx;U8|^1<3H5w=ZG8ID@i?YUM+o z%(lWHOatnC399)cDV=#eGCX*-_glGw{q=si1 zV&EqrzPLyA?hAKDh0@!q!(vDGR+v0VjW6}ZamBAP+UPRQ^ z$xvP`Jt<(y^UP)x877moPl5?+O~cldf@xGeijfg&_QkmruGs*Ezk)$`9T1>k%|UQ? zkqsYf!Mk0@A~YD#JY;cFy~2sgk%r&Ntk2V>hDyV93sLHTwl4Gvjb<`?z04+s`Tn`emKtHGYpO`ln-zoKLzh`s& z((56Y>>~oQnf{{6P_ud+3)jkdKhWOmurQs&x+eMR zII5)r2>123t0~uxTyxlZ2VH!&Gqqawq?P?0R99o4oM*p#$W)gp)c!W)%Sz~jx_0;ClEhT)1bUO>%R^UMD z`ONbhRU@kuP%TAmkG-PyoID&?eYp@cm`J^JWAo^2kbNQT$ZStEYfiAKo2bkDvWJRmV|=V4)YLo3K(vUq+u%cv7|Zcw3Vg;D~&I`X~^n&^_kfy zw8|)IAMULBU6xtPEZO-{GaID8W#(KrTI#1rZv5RUS}!yzpqQ=S1^pE=y2Lu0N={f! zfFeq`pkG-QLToRvV$$PfclbMlQXh6@jSEdBrgte*`m5UN1R}JKxud@^#rKpC4DRA#A8_chmUAP_|N^o-*h74#}uo=-YkZCum9)Ol&nY;K^M=DJpS6M7agBn{pIe^oA zvv?K;hr*a_GR?n`YPPApYFkLThi7&pyLyZm)iGP7Rt@?&_)~crBQ?5NTL`#dU{7C8 z7d<-8M{T1p46cz3IT#eFrAWC(W!P5Z%rhjfMou|<4>e_U$dD{hKg!`-+A7VKa)Oq_ zJS&VY?@y$k**|M1R0{^x@MJ))+!%~N^&m~YT7iaomT~}5R#tu|Bd;}__Uw$MwlFVe zgLlWiAG6$-Xzp3^tWefudc7wKnDxApxbvW3LA_9eZPr{~nwgUVXzi3-+ipZ8Yb5(b zfD6PqiJ(!uR=v(i!m1&c&lXE}Dy{pNjd3^Ild!J-bz?uCf$gS>zTAZ+o5{$WxiNP`&AOPdI0ccd=9=G^i=L`o})nzAY_> zEXUDErr+`}DnxP^FCAwZJ(9B6s^xt6LSQ*)sGG(ZEE$XI3 zS^2Tt`!?om<2>20R+p6A^$IXviDnMguq&0m7b=L?nrLhT4m2=iyT@jd=xOEqoQ63w z>x$e%WN9EqweeN@<10_EYcPmZE$;5<;pUwc$!?Z7JW2#w?^VLz*df{!e)@UXfKSMjzxFlK6kcF z7VqYMH|a~AW-9XJUMrw1SC%^?SXoj>l=fnl8p{D!SsNA=g}p9K&(Fc{d31C0_*&bv z5ZuP2NU}>jERX&1D5Tj_d`s7j1$E|NfHO(3b=ddL8j>fR^U-|{PzkoWtoEbP8MQMM zi8iN{M=|C5%`=`*)nMN_P_*M9h+fvtTBMp!T`0>-U08`lR?(n_Efp2%OIknxD6Pt1 zmj+_u99d6ly|aVI`Z7Kymouo}7Fi^M5hm=t_HsWduSGV2WtoMLMapxXU3S07&nSm6 zd5^_CK3RK+De#}?`OOY6^WUr9HGiq``y%san?GdPx!FBfa%e{LbbM0QtevKuEj#Go zJue$irIyTC<&jm~!ypy^$sf{ImiK5iORCK9RU;;u)S2^*j%pOPffnY?%`#6*k(#l7 zMTmp-wJ=b_Y^}J{*-RJ7EtNoerD_z{3quNRS-e(I=a6_bb$nhwi44Mh0GYAj7P(`b z7b5s6H&GWF+k3&p9W^MMoCf`@sw(*)FY{OcWsNw~vdA;IlHe{k+Zo^P^Twkwkr50i zhih0~6xWN9c}JsGz0f9$8sIRiIhrx!+AKu!lj-$1Zk)!Pd!nehNis{iiXKzK&ZAx< zR_CdI{X9}NPlh#te5B_$ySE`_V`|MWGbvrKVB-vqjI&yYd!8n3aHiF2+A=>oc`OKZ z7R}KSabwP79-Ki;QZg8%LJw07`s{e=zsea`pPN4Aux9H??&0kM2^3|9ppqJ&SFN*H z$N@Oa$bGKGEWftp`u$-p^NGF&L#Z(Ag1;B%GbgtD9yl5H4Z zsNSt-phy5#%EN;uVl%p;Gatf6U(T_}#>kpIZDnJ5F^$SoNXdzq{tBgR>t ziAI}1l^Kr=yd-J{`sLj7f4qA)PUJ+fu$CcgQ)Mp6;YGoZcoY@K;gY;DW zylN@9)X>TcY+A~(OErB}sotz$O(l`uQgq00B{y_ypUXDSI-5~mm9!f7iqLJUETW(u zF1^=AEJ|vbi_G>UXt+qPsoe-in5ptY*9bB|$>PrrS=5-iSSuBxWVQcX?8L6BbkG>B zxybUNkc1;40l}$QOEklmqakPZh}h_tnj#sVkT5&iYvu!{Jmp;V-N|;LF`15iM#v7s zmjD!kWA$AhU%p-B$dE(PN5R75wPDFE#e}LF&upRoG*^DHI4H%clgBeOS@mG9FS1bH zEEqdM2&26;HzTSqBe9tn${6&{%=|}e&b^c7Aqt-w^M<{Y)lZPhqTwwLA9ekBhaBiiIQs$y=$+!`iL3t#D=TFVr zC>IgfB2##Q6;^@U_hqfMBVr_~tS?dV>|P?+Ol@TfJw1CLe74rJh#l1#US?gaY+vfR z6_t@1<~d=Pd0U2?ocgtuWri4Y55$9kT5`H43;pta=d*x3{SN93o8`ip(cnOA-DK7J z$qd4}a>u{cduhT1vP-Ns69lYgP0x=N=v}-{a$L}NpKLw9Ok*bspAiXz5>O@A01cg1 z8jX%iESi;i{JPE&x3Otw&wUn}cJJ#XJx-V(p)Uokn&Egj!1IdJ^={T^|Ep8goBaMc+`QsaKK7OL&)dnPrnBa5vk#vY6Bp zs=6lDsIZbrU3e_UJQ?N$qEzl42l&8PqI{+~1FF|dCX7%6vVNwsF9jTwHzcBv657D6gzKL!C?{ zAQ;n1!`#e^P0aXOmPe|@luTkS8y0M{HsldPloWfMHFNi5m6RngDa0}C69FC6+b7GQ zUnWA-c?y6jCP_?04gRe&Bs+V}rK?yN&8l;c)*)xb1m=_hYC(;&*0$07z|uXKFhloy zl@H|P=gGnm87*q_w2h0lq*U&|J8E1Q-jr#WMXnRo8tohoDezTk#f^jO76!ZJRq^CR z()3Q;ES*8AftH9v2HJXSf=*^@bK%`dPEM+cmGZcwMx2ipf6W-J(7)uAbG>xrS7q%# zjhY_)Gei$;UePdfL$sPlXJ-gyhHNf^uQuwS-V_XdoXs_%`yw9G7VW8M~;M*5f)Zy_9S7?iV|MW7!rsxlclkUbeqP} z@04p2^kJzxe=)v>&`$B(u!yd=)`-M0qgb)5T~m8{y-W|MIep;CA_AOA_q58jA$m4^ zzGgM5lCkl7jFh0yNAfdl)Lc9VVv^HqGlZyycGqIabzJ=S)L*i0vOtY^NMFLr6_gaS zqxf7i_29aTUf*bzYI4)?({-ad%TW|4A0&daM)Fq9#+K`i#-JZ|XK6g{4x{S_b(Xp* zUBBZT;vA=`@}vT^^2ok3w3zb2pL4qbft{e*{w#-ewW*YOHkawKgN>hR$+h1dkf)Rn zs5L4B9z}Q&+g8lm!p+ig%HMptPOly&)sGeZhf#;51UU+MAF@-TbJe^W5BpQ2`RqZL zr}Lb%*$i_Mhzxx$=vP`#NIa6!f}TIxXwCVYapQa@3_8k`A{unIF;YcX*pQBck()(i ztJEDA($<44QmghvJyF?qW!4!@-vmgz4(dD&=#8Y%#_psuqp>m~kR=6}IH?2Vv9eLq za$lH;BSs!sk1IZgFMC7eZJwN98zWFy`(6D*r5)O$axF~r>&V3N$P?AMjLTYgG4~7d zCuC_fl)v2dB9TdVi28Ns>|Z3f(m^86*yMgajVU>GYBa{K(zDs?UCcSSEyZZu_ugx5 zS?0;!CVNz6nOVCgz9BI;=Ng{%5=S9gZRxB>oQFAO2t4-F;sNZW1N}A-6Eql3T$uD^vyuav!g$b2aeq8##pX)ch8T!> z))NokuC|^BG`lyf&O(&sg^u4j>Z9oxQV9?c5CB5Cec$KVOCIw<8ooOmv>uxVN;WLJ zPEF8ai4k!#Jr>F>a4WqP624st4p}Xhsfm~IB(PBAjHr{K3s??TP99=0`@)DX>LZ`~ z;(eAn@-$gd&$xdrd+4<`|c>DwocX#Mq1RgYN zSH|=l8~&_4PH)Fq>%#^no4oW9LAht9lN1MNKt;YNB1@>iUlufs}@t5LC?J*$F zo|uX&$*ist3k+R1=+{E3CeH@~;<~>zeW@Hh?e)z!HtFdNQ>}YtF%zsi`oY4%UT@fo*wopst^Y64Bfd*8;tpRa&IJEaCsBrnBU6cODwnt#;wCm9nD z+e608BLLd)xDThuoVN?X=$g!`&qiItMRbflw0#aNZ4iTM4i2!CP0m(PXD}wcfNiYJ zn>S^^c%977uh#iz?AE5w^5V%!i$59&Su@ZF(2m z5Wv@+#=es=s6iL)J7wRfJfyVmN|9Hb1=M1@vYzZah|Ywoh;MFx3iNaE|H^N>;Jg zEG!tJ8AipiTN^Y2tylwOy6A1z9xgfmhY0X?@iRAZ-s#7J@I!#Z7#f5J&=)PE{D4Bxl^U8+LJ1plG;%^h0qgE6o^_*P zrSX}&gP43ma6BSe!L798VOTS zY#q%4SJ8Q2jwfXi)WOOsH!zZ}k6A4|c_C2h5O@yoRiH zybL3^*XP{aJWHNzH9HDShA-)#A}agf^VoO?t zyYD|}2pkdBgQgUFN-dicmJlxs?KBP)T5a2n)xytt{tlT#%6&PkAv$DjhMkT_M@Q<> zD2)G4vta`q%5qifX^0RHhyL|O*j%q|U_1Bhty>E!^9*flz>~7&-YV6(R#_>xX)W(0 zZ8<}G$1x5PqOR$nXmk=2CY}K zdLj{A&;rf~P$y0iqYhPB+HLWBt60PCcyN|Cy7ois15!lPFNa*z@1-1rz8$IqMS9~| z70+!sFd6l+5E?=3t?Af<2eC?x0wX-uo>8SyAz=COce8u;Ft9YRqcyi&U!_;~9=2Le zd8i_-25|-z@O92efo)!eGkAVesCMGvmyH_Osik53$T%%JT#D&-I-HaVJ{&#EjJnJ5 z(5Z3xjoACh6y5Dy`$7lQ@7N`GX^r^seX!KG7%*EN1;G?F8EI^bMn+m2Aa7b1KCrvhqIx-sc`lPhZf#}@TPvilOUGhhZQo3|RaogAoXry_(#af-vI7a%qMC&Ab zK@x{vrZ9|${U|x1JEWjubBag;HbW%YT^a!`G^wga7%Dcmblhr7HZ}6}#?sN^Ba)gU zi$L*v(hQiGF49vTo(D|f!Fq^dR_Z0ffU5l| z&%V)U14X`-BwwzwgpbC&pHbx4n$@M=w^D+*INtry&8gQ*XRb{|TSVAOsb{|fjYm*c zc@~>WaMO&6g6oN7zzFp+%UNGBxJX*p4l`EmSx$YqyrN$?EjMOoj-6SVrIT(QV6vV2 z!0FZcu&u3>z(@x{pqXx+%u0N7Tje=x5xDGA?ys6OU{FlOli63Ba6B*WM;Uz%#wK7( zasZ3F4rcQK_YD^X5=(M5WPWOK2xle6+oY&jjLpFV1B=oM+{#DdVA1qTK)`5uCSIHD zW4H*TLx!wF9Ttvp4)l@%X91Nz+;?u5z&OP_*o0vQ65E~40IPfV_IRjeuTdbW%)+c9 zyo_2GB7i4XPtwZ>aO&C$G|K2)#L2a>DYulA5@^2NcZ%#eYU~$5@U_``WT_f_8f#CM zuKM6alxirh%7le9WyV;QRNp$qxuAA;6AlTP9;DY$6M3?+T+-eBl->(RvQVvZ5-M{D ztWI)B*n_p}fa74bk8(0I`TDAHGxbnvonvFFN6~$nh2A@L?6opzvawo@Relq}>6Ool zhGZ5dQOu*Ivw-MU5k#&c04^4_GCG3Y9zw^M6=x=^oDl4;Bg-EdPKX)tGpLQI1k+2UWy~}th(sd7!~?*( zUWd~ex|1CHLe0FbdM$x5+uTB;9#b^=FwhW#uCE1~OUF`7(@7H%_&Uk&MUZ#%hvtV#KLPeb&p6uO{MDMy#$M6o#pVWblo}|dv$k+ z0DCD2R8!@=@akX!ov1LOuW{s-xJS&~AOZQT+$QOxxck{c5NxFxw7m5A3JGqAj zVa3tow5!I7#xR>O1weocjQi+*0co2f>9e5*_LfU4(E*3YyMa+hI-ymB{+!<-xhW{D2S^kNsX-@J2K5K zy)r_E-mO9RXVw`qKUY^6kXg)2dcEet1{O!jGvDnp#hq^b+L01Zv64clCr6{ThCzIS zh^C6QvP6pXRP?W`=cd{1x#yydjJ#A2TT*nc8A2t7td_I!m~RAhDl3D>KTlRZ+;i^J zQb#|vBQ-HrnQDL17!2T~RwB$8RI9%tS5Ro~5%hJmGtVsWasfpdyA#|VAu zdaP@@lDt}5$wbuk-Vii*?7{i#A%&Xh|QT9jG9dbIUzoibz@?}+|9X%%5XXc za=Ww>DQ7$2Woqr#$i4bH`BZC-fAQ zR!q6ZJW0rzsSL=cDbOz5S4z-NkiS3nvO~bK?WMl1nrD&astPIVJkYNQiV#85Eh9)W zHGY^KjFZ)|O$owrN>X!=KYy~_XnfD%oV84ok%3F?3MVRYhS@<7dW;BrukvUiL6btX z8tsErxNTvpT2=fN5Pzp5+s53N)s(gA&!pg_qw)E` zYJ+fl7nfz83->TcnJSFSLu@w0Zr_iZ5r(3LYAeD=C)VVa+E*;58ZIMx1}3p6n;|o$ z)Rk45MZm~(@ax7+q{pnChpIeTA&jYc_Aosuw)bX|!4jh;2LvT}v$6o_%B*G)2*?zz zmDXs@>dO5*haN=$Ga#qZ1K42rz&;ewPTO%3Ts9SX!suv2B7g?RrUj{`767h2PyRG~R{v?+Zok6xyXi67L*ZtzaFgeB=yfR>{kZFa%>8(|ASw&uZl&S@a&w z-U0XEgyp=glnt@5pRN4SPj12$IcDrN9)j6c>#WB&*H-Rm<~dLGu5~{Lvf8*GVvV7* zX8Weil<20)%v!T~T1EGU?{M0%X*tAcoHB#6Rt8sR))TD5RfTrspbX$LB9Sw}$P-{+ zm|?0vPL}^tht!*FDJMA05duc-UM<} zG;q)e_8f+kHFm&Ms}=F@xio$h0Q_<(#-0o?~k|UzZH``bed{DRLbJ;5( z99LnswzU=}e%=$?VG`cop9D=V)+^PNXOYAzEB@rBUzpT5q*XwRcOXCQLgWd~tcVYj zo2FG0%?J%!4wQ)BUpKR*stT*i7{w?wVWQzoGz`6LB}fgjBRf)?;V1GF0tlJ{UZ{Qs zER2BgPG~~y_7EWUtOmn7Py&L;H!SkX0i%Nr2gXgH5fYV{9B}b|HfqQWM_QoWWaOLPD#XwiNM!qA&eCXM^KmfbDb(iP*;XH%mu(ZJGOO zeDK7^lF{@Gu6^erZDb25y7$LHu)+tQKh>Y@b&x%2S`1M&karr!M-wH3?G}wkiSS<7 ze>A*`eMRAaN%S< zxjWYPW)tsx$W5$qPmD9cSk4~Qr`C^@RUjgR8j>mFWSNJfYc^yyKwqE_MEp2IGRN~g zh^(@y9dVx~2V_fSJ5zcUYkeQk_o^_J9_A_#9N%@fvsg6b6<}4(Fq(blsG(@?md~x$ zSPk~Ao0BhN%|RQe4Qu{fIe1J<)2N3?4wIIHv9{_5^{IL(D!*#Jv5%jwAf9;OY75c; z=*QlloLPGExq~5$jMVNitRv^qP4mcPp`O4qt?Nx9*Pyo(3KIFNC*g%@eL4Te+S!!AZh!JHL^_^1ZXKq-Taxa@R zZTiF5rwB`P9;gwEw8p^FY`+jy=2;1l5Z65&21#mh-XN@Xd87(XQCfr`U&?+*1zpsZ zQ85CosIqn}kRB~mMQ#@`Fywj{YV#skPL(n+iL_g8#^enGo>&9M3W1b$@i}CpMeaoI zN7pCtL(DQ1{>VD46kIEeIoKWF&b89Caf2Bxnm_032xmj^(iVZkN z>J47lr0gxzVvF1jp6R1X8Krz|c!`>j)!&`<)wyv`jcbz0akCo-SujK%;m$~Bt82H~ zz{Bj~?s;$@4iPXLsn2yBiR@}>?PLMU+O-yC6}f4bx&ww@tg*_*TKa%pegU~hOy!Bf zwJt{IZcIT+G-?Zi<@xfxxgukdh%-?l@7(F0JR)J9tR1LGR`0=UDLuvB5u!e> zu!;iD&CAwA;@MsIQkVu8uB~8zBo$GQit#BcPUJeK6c6${ZFBFJlrb8^YHT!mnzU(q zvmwh|e1<(FT~gG;OQ-hbl;;Jh+0dgIFksrG;wVF9_IL)X6wE5}E8v)Is3D$Q#E?gI zLPpjC)}JF-&2OVo6r~brFD1cZz+;foQkh4H&z9Oz!J7a~fk-&YIJi@H5&=RoK9iNa zl~_s{$WU5U8_JtYlZ8B^DAzvHDD0hjry7Z;**$TU5d&m$2+|<*jCoD_+0IG~H1sGl z$*0IQ38G)Cfw(U5rRLaLwGz%2L5pize&24d%PIsQA$bb!aQWY(txt_Qbd4y;2 zD3J5fh7@?soDBy*aE>zFS zT6?uv(sNdQ>7{s%yqM-vjUG1-6bS?_iMqlJL-k^f7CT6_)qkjIy;e4n6Pe7$l}(7f zMG#b3QVdlY%z=oI$-%gZ3AxGs(hi41?JbVX-MDWrH$128lo$aLnT@nt3loltEDEkM zJ%uwX^|b=??oOHHHmkmA=(sUcJh{1FZAk>-$rOH%`>{&CXzsP&ktZ!A2R|Z;o#k{~ zGAb4D5R41H%QNUCj*IF~z|Kvwo}%iS2y-sE$j*{=;y&;hh4<@4 zOhRJX-|^Aq+n3bsOPeXl(9W;nJr~J}+J#F|A#rB4%&;)zb5<&b1tXoq!jxV?9#EsR z0)ToPL|@N6HpvPRYSJt6Sm@a*9qMVOcV|3nbOpvqtgMJ8z@8_m0n}fmoOp{XG z&28laX#2BkTW}XHn67}vxyU_jv)~*u3{qi?jcR#Be0mMjYXhZ*#=@KX0y9ZDVam$= zWo}k2+-pKsAlf}7mZ`}7YO-)LFtetsx{zGu9zB?$i-l#j`qAC=RcVK&0|%r9G>6<( zWkKIE%fyVr=rcS~Smk37X{E{#vU}?oBhx5?YyESXT8pC?<$PP|&&hdVz=2IGI}NMD zJD{+Ve9D9&M!2{J*Va}|pQbs#3$NJ~y-St%A@7QzslwZn`=7eOV#0iWHyK}V6>zy9 zhUmIZS>JNL2sRt8l?p%>Uk04$MiV(CfiqjChCRYNX5cC7Jt)tdRBO`E%elyE2R5ih zR(Sy~I*%sN(4$yL?#xm2l>E7(o>!icbP5L{RsGr2XPi&38t;@W?~|QuUF4v2kXnZ+ z`!tSP=1VNP$<4N!t^yofTes4EPex(Deywh9cv(sb5*Ox7O<5GyP%)^>W@&)|S(Vaf z_N1D)d*^tzvt~%;nr$jxD?fCZ$%wO;rc*hezzn}zGEn#@z!roKi^@CwR)@QqL*+F`b$xLQ7VrPk^c{Flx9*pP!smtzVCt8+`*^qfHn?eG4+Lvm) zn5;P{QS!-ZHWsumv-DW3Tfmlc?>fHDLE(f(l4&`>7eX`IUK1Ep-}$k{+mcASXbKX= z(6baNrzZ1`^Xnp~JDA-og{%RF+JH zn?Cf8Qq&<&&&sF;My=gw zNHArV)v0qPS8H_Fs4ShJrOqO(KzTH9q8A99*=o?K1Dw<7fVC|(tJn9*zsgB))>7P-od?L8VkaoXHkGE3l5NJU0<)1l> z&M;FBysuG&NhT%nXY^+h44g0P!mPA;6vV7~Wlf%cnAc$<1Ev<4^)f|TqjjW$wcDvC z&GvAgm+ShHb4&i;$^i3tB25!w?^M?G)EJ)0=Rg9!gfxB8aHolqusOURWaNXr?floH$n49<{%A0J^r7>3L>B{kc zd-Z@3bu`)pntFTkScX-Vvg&powLp_|d^UBX<}#2^Bof1KL|D-8og#OF4Ro|{%tTd< zik?X-7$~)kwmI6rMQtHG6`W2 zqs>w*i(KSZ{i1fnt8odLbW-L+gt4<09nH|4PJ$r8+P!yDw9a(^47=2&8Od-oE-*@E z!K^ZFbPU)5SuIiWiyS&i8qB?Z7(O{I-rw08iGwl>uMe*uI0|ERjQT25nSlsZ#hWkL zyR$SIBi2dM(jH7Y%a~@J%gO`v$_#5$A6-kI&^=UDmiU_To+*=C0%!zQ{YqI_X;EpM zdsF9(&ke&0%rK_?lrGBPb^O(oK@Qx zria`@07#ufiuYMHWiG1MV@6cuCbI86oRJc^iC&ZH8_SjCj7tf9)2qpCuE5@?Zt z3}_aIKTGz?Eg=dgN87Q>)B^$triRp)jAY7+(2aDr#&jFl);bR4lO_FDma9<#AN(FH zvoO3Q_D$XhYz{=&Fqjf_m5{%a#ak1O^(N z87X=p!kSD_z}N!4kg?!^^em+9lQ#xI6n!wjmy*58_-;8HHd4*2bOZtX!ZTi2f(WES z!B?YWtnWTxv%}no&2yG8ETg$wPo<5+y6aAc19iybvqwwT2$4luJ~q@{*NQQLOfdVY$>QW76awHnZ) zvszbbrC>nfc2j1E*fV;>EbBa8T?Yn3hGB?*zki;Eu}cZ1j^{rd9oVR~!0P+1GsPdC zOF1Hj76zi6p1rXYEm!g}WX`$%#NL^qV$&!+HX7(j0s9L(ul+C>S69$BxZVo8?cJ!~ zoXV()0YRQEqAca@g&oY8NkuAvpD6e{z$+!gKc@RJtDMjN4j}dS&w1_}3wJEsq&z$0 zrk+%tlNf0jr6bVcqV~ktW+j&GJyKrf!UC(@dqqYTpRF3$pnB`j7rI9Ts{3)3?uT<) zL;!Im^RY0zn8)`((%4iMKPFQ(^wh|F5QGWh!6_FjDSQY<>TLWt)0@I;EF?r3e(d?t zY~oM9C`4e2MU{O)`iWKx>Re(LBA6N(a48sIllG(KfI8KiDt+dXy7TDlE9ESs8eL9) zKla&E_5GBpvqP?H%T^h>8eZd_Z6qFEDY%JG3;Pyuc)N9tXwKSvs+nRJ77_=*kiNHv z8F{T?u$8p`e6|UQW(}7pRmbUi>|rC4Mk+0p6?uwTpULP34(TK!SS^GI7CDZ{aPH)J zf@Ij)h;Ir{Sw2ct#C?W~EK@hsmZYjw7IAHgKEN*qoOfG^Td((XjpEB_xh>8iXXadp zE~b7*SipBZEgM7FnOMYr%5+CO~g0*K$&knplpOsXkT(e!au65GhLBK5?OZJbd z2z~gD+N~F4TjxA8O~IqllD09qsG2GvQf9o*Mo@m7|)rGGx+FsKwiNRvj0lMS~=V zZ@Ec#_YbDJhGAbWRvA*nnRRNIBYuUR?d-79{30n1=Mg0s-6f;zG4zvJPP^SPjnG%C zNl+1R5K0e%gm6Vf*%2YaW`t~e&-iPxiehu9R*~eW!NEsK-Ygxl_9r(PkA_MVm5EaA zD02%98d3*N0(K40--L6E`@fBQ!Vz9=C0`&XF?J@MEH&}7u$EhFLKtz&%7tl~mXRB} zpWF25`DyyhcoQ3InYt{~^msj9kJsb%b9s>=ufzE~FLvo4sb8e$Ri9P7$_6nRL zXX~Hdfi`sF_@|mVS2!e`4aRr5Oirt5Ca)@t*Y*MJ5w$kGa=cX z+GqD@^`@mu<#%!oVgK?jT_etUwpRt3&=`TjgXEwB17VR-v{`iZxR?7^!FkI4J}{0{ zn4fZ8Nqu3Z7+E+%!INszbD`bBx~2Y13fr>_>0M59QP*I8R-KiVDVWa^9?|Tz!gYrIeoQw{ zYW2De%XRv(^M}(XPnOr|`@JHd$LsNWydJNg>r1oK!mhqtT%|A0+w^VYr_(m=!(O+l zoe{({j;9WrTupiSWCZ)tPtCvkx%*Q(zj{#n6L4xZnSD7GW>A;8_w{PKQdtOF2n^5X zXfi}UW;w7z1C0?apXe<{hL2*Nn#@Y8me&~n!NcER*E~c~*AyUR?n>Ib1 z2I-8U4$uTf^TFrq^x5|gkwGm4XQ+-cx<5oi&q!}IG8}KyNOO)X=^PnN(|SPK5`}_r zCUs0E;U{ps^{ zdiNaPeR5&_@p`--ugB}}*QEirAZADau9xX2n`h~lm;3bY{XxBtQimDyP)n4RsBMQW zgVw()n{*9%hUV;WIH!k`YIxXkgb|YFx&?gOEX7_(=bg#~)ZmtyLNN(_g7LV>gE8OV z?ZRJ1nJG8G*RyH|9)w1taJj4`ze2b4?d`#{r2+!$nM4<9Vi%5WFZyd4*zzoT=Poe& z&%WHJo2Z!510})A&c(FiBE6%=wrzUv;gtk@Z?3ih)mgudVrHkNPqL{=IPeMwIJ#RK zxgZ*4hsXd?fu>pb3^X{IA;iSvX&{X}q?d5U{%*iWCp$V``%2^KR5>P}e{Mj-D0N*@-&tcZN2J9-eFh zT74Ec{cfM0J-?1RVJ958J9z_;F$pLz+K-jyE+tlbU8fA^(= z@#FP)JzkI3&%tFzs4pBCWsLiFE#!>;*s@w?!HO!asevZfAG_okjEJ%C4tMIgr9}d^ z?I<-QL>MaDbUrr{j$3MI*PAr#AHt4bMWB9CQhZI z!IP^t>xA{{&FXuLuaYP3<3^=^EEVA1nH6IOq5%5r*-fOO@$(1QoED6fP(iP7FA*6M zV-pfoWaizpqE!PqN(rH9qDYGr#L1L-&2;he&aqfmH)}CzfH#5v#L(e|Y%G;?_x^)E z52f>CI4dFybOJjyIghzHYtY*8pS#HR=>IC!)$eCMpGHx3)!;g3)*WP+lZQ{jI9gU_ zF`<0XP5=L!=<#~I9K9USgl$M$1DcSFQoW-~2~j7NTlkmzwoAi@lX4~)CHg%Y;Oh%rbud|8!~l8{ z4&fkX2GpTb#Y`C*dTCX}^CkB#z}JwXGD4k^&8Sh4)bMzA-w~648%wmzg9j5_U2W1K zoQH{r}LJcFc6u z(*w%e?v|%cw~KONy{t@0g_YS@h?qJc^+4)?(pLndEzPEyvjZ!0zk+(TG_|>Sy>T9| z$LsNWy#7I5^K7Oet zYn1VeQTAK{*y_J6RHD+P3&?+mPO3o6#G^fn{Of!^F)Pe8+_o-qU22ecDp)0OA#@H5 zE#jA_u@jY%YyoN|UJ`m{k>~F;^<{9lN12l*s;?=TLW$=&hIQ{4qq1(rJjAdgN{UyV z3U``oBz4S2xL*y8I-Vcr#Is9DV)7Ub&w&c(d^iYm<$Fn=Ar{svubo737g_#^Sx&LgP`&wH zeI|^>&eVTtwQ2)n+KHfRrG>BG z`lSr%apI0q-<#RRJyX-MEwdL0F0w?2Pey1TtTOoeif3J2ebAna-AcBawd&P_JC$54 zl$;mg)s^x3{eJfk_hFp8#7f#PsEOwtWz|7ao*CD-41MK{DMVt~9M$V{Hf{Ng+3?|6V9LY%~@KarfVyw`qE;=nnzC%G^{t zb^OdSA)j=;0pJls%>dO6OP#QE?Oq|2`7|Knn_CD*nP%R3AQqn;a&16n67uKTO*tQ& z<92fbY#RFYbp&seL3>gPp1S|uQm-<*lW!-7!2}=Ywv|hPsGXT{W_EopEA9;@UV~>E z>}azdo!L-vdt46FMnDAl7w2EDMRFKP;9@-G0Z20g2WKw!ml68OD5ev)pC1P+k!WMt zQ#gjtKMj6=VP^S^+i{bc;1aKIDe6;!p`bowgZ;kZmAX5OQlimPIUWYpyz||3*35kF zRUjLPPbvxcMH@?5nJ}V8?9i5zj>qDTBJ_ps<2eG!*MO!X+wy;8>!;h&A?Zzj{gZ#B zEsD^e6(~JhCOaG#GPIBF!#WFG4g9A4wrBTOF-}Hy?g#Ks^}dqzM1Z*{;w`t*#B&59 zqP?0zu4pycNgf+V3mZxsQ-EQua|?yyebX3PMrzkE#s9umFERW5tn-SEHVyBaX2BJv z{Lgb++Qy_>M_n`qI)qqO3w>MeHIT4>QzshiCtAspmiAhYoKb?ZS_2a^*)G_*RlFWH!-h6lUj7rPL;g#oY3l6j)!nXTDdR^!{TqPlDB_woT$%*iW((&c!!= zh~%0}hQ!6CN_;+Lh@?Okj*};3UDUM&G9}90OqCm9>k>E-U`mKK2Ou&c6jN0mSxgRa z_J-jhxNR08ZFbU_hfsX)|MGU_SI+PCj(fPK&ulg?k^bTMGArhdcG?E2M*uu zWwTXLQ~dqGbdt}i%YmO+N=1ZB+SqY=k;h9*;NeXOL^5JM980-)nF?&yelX24&BtJX zFP`9IcXd>#LR9LE)dVRK|YgTGiZpp zBkioyrdUOFAiMO}U$f{;IGvk-Hn6l>D=|ehnOcDyhB`fSRR$vO8gLyQ^73L6(AqLs zZ67&lq}o!Qi#DE?6*~FFcIhNo0ix~hhH!h zs`QpGv@wax3STyj&`YJXtQ*g9lrM9JwO5z#?Nd*mX@@V(7Jgg)X3fYiudb~e+o#&& zvFH&ln_iL`r_~K;(YGOum9p8Gg4H&Rd+AE7O)N@`V{#jRlCS8pm7c>)MP00oDc0HL zC5+s^a8(vCSUk%T6=dI{{ zzECI?fZG57u)*73jZ%&7Xo=oug}G&bZL&DH>FJ6>!$OYW?B)y5;?)8=1IpTltj|+C zv%Gnbz5fb7$ZHo;_@}2`F->u+a#CwN7PMU!zL-~U@iH!#bN_1DHIYI3xZ0x-N8XK) zYrd5S9s0+`EUReW(h=+;At6+tbQcbmD<{n+L6%Vke-ZIXEb3<@hp?4XV00KEZ=KG< zL;qa_ZOeA2#F?vv@uJzC&Hxi%4yaLt(^g0-g_P)^&54H2WokU?h0<|0%`;q#d&BRidXmJGtJ3Vd|Q1P|UqV zi3gnwx|IWer-szXYI%ekX%gNd>|=)@x0!&^sHLN~i$3`);m7=S6rEJkQP!!Ivi%H0 z=1Me-Uc-Y=T6TUqIn^GmJAn~34_?A30~!D*(O8;sCM_Q1tKXEn3;ICMY3Ag8o>>NW zv2$(^f?k%OUBFEc5b&6})il4?`H};4Qz*Z0vv}L^2{@a+_}nslcOl}8Um+40)RPV? z@ir<-Tkr8`9`Kld6z>zTKWI?3jb@#2`{?ym@e$*(iJ`pUuz42vkoPVywO=5}nRDjt z)05H29S<68vi$90JkqrpyfX;=yyd6(Xc0}qL;yoz;grJdvvhY!MYbPA0j-~<`067I zNQ|2uJv&}LEUKLeW^|L}wOM+%towrRm_K1`gah;XQXxGswk}y3V4F{CLNNXZcEBXp zrpPKnlO9uSjDUsSb$&m>#%99vby|A@gp%&!yNg8CB}H1>7Aq|q-sLXJiDGcm51-2T zIdPfsuGAE_jR-=e*3bHG8jTiwv&hx*nOV?GI9g1hK=+D>JsU5T^|*f5vY8=Cnoofl z&S!5>%7=!sN9M}K20mO6e?VR!E;*sE*ZAEEGM1KVScp=g-_u_ocXi!qW0d;KLimOw z>O@jKNsZy>C6v3Mp)R+%L92zQs|9o?5(JNP1b0>#m9oV?)AXB=JJCA=t-; zJ)@FJB-L@!52S8iRs-y*xU8*bbR45D3DrG73tN8b-iTU%9n#)w7dc}Nc|NNWd*~^o znK0^vCAj0lL=F!1P#}m>t&bUM6g2)vy|Ouqr)Dem@1DYz+8x9IX&)SEk=c9kIhYZ0 z1vtqy?|H&_B0_y@g9^-3KnCAb!G6#Z3P{Oz0W|9Ei~?QZaT+KB?YL6eh9joEV zY`^sMv3`$#oSS{U+0F~zr+77qIX`)PZRnNA19)c!FA4;I0gt%7T6RK`C6b{BZ%5mT zfa}@ryXL?Lj4}x@1V9NHr+~_WRl$~EyzNJkIl%o_Xa?wFR;Lqkz%ZW(-N7gaeFM0G z@quCPa;U|^*B}nQZawr8=NZsp8Ma6sNgNrl|zgOH^1TYK}HTV;?}jY{S$wWJxuzi&)s?0iJg)(H8Pzl&YDo(fqXJnceNS8jp7S%|P5Gn%Ms)m!pSRabjWO6_nLp*q zZc|SsQ44#zNalA1YmEGG)X$&qZ?As7?2YFsGF$yDa=Ahe^Lwq7)}&v1-)R3s$?T$^ zFW>E`DqG0j8guT1^LLMrMtf%taiNVdOIEm?L;gVH!5eZ=c ztb%FEYwhBjP|9%1n*L61GLbnOaz)-Q#7$qV(}^{-?+5YPbY#QXEulNhuuO{!LB0Py zyz-ToZBmmX3?g^J?d1?*Xn)$hA&$Mp1$tg-X8yKmSCyFcP2AcJk zF=n+h06;(Fu3Ee`rod_500ahUR1_qz-aj=%5AQ(d(*S13pSP#wat-JofeJV1(bI7O zxS)JC=&bK&@Y&ZuFK{`->r^TT&EObxH2Zo6%;~!mr~vF)ypaI+x8J(v9_c=QV+x`T zx{7*}2ipL4+TIUgd)ox{1>M_&?fM?N2Fh+$`@GRao#2#Pp*0)0+9FMg02)O_8sTEM`8pB2y%m7Ua41 zAd@$e?bKioa9=s#@Y@~_G=@U)tw*9+>FsbEsAT z%YO5p2!onxVqFT)QU}_YIqlFsrev|u=(QPaVMd~QT5RT&cf8LmddO;J`123Ox!-maEZj_TN& z??(T84M4U5ysrnXvVIH;+#j+1vF>gjM(>lhxA#sRo!>1&LFIAvZ9#VM(3kllf#FF# z!T*zg%>;&=dsGMpBcZzF;=utA9D??YN}EgFqhkZ|Y{u8xgRJKfr!9v*a}Mi;ZO9r> zh-GIFSlX&{TqeLQXT?Xq!6%Z?j`OnI+#%f=Y3{25!T`p@}?ZNZZb>f6wZw-yjbdW~NaN9nG@o4u{cjf=$H==>Y) zrC8D+=yV&>3{(It`hX4p+rd@HJSUNP=ky$IvQG%*BO2Hakjr+SMPx+=EwsMrwaXxDl+R!tMieZ)`;o$ ze2f^{ZEvP#f6T?|nS`%LP-T(6G?nwcp8SzqDFfkQGZsNyDLx3*uP#T-3Hvab_Hf4t zM&m$A^@GDQsbQsDyuI5#{xO}UKHj44<`7)?H4br6#zeiRhZ%m)Az#HzL#%I8W`E< zr%uymwd0qe6w#zCOZ%nZy~B^JNgGA=+s7V4mfNpEJf%@zp>9Bp*VZ?q&Zpt%-nqdC ziOk^9N7=%D=xd_2MPZqKFgFAMJ?Kk?zV_V{^bXO%Wcz`Av??~noq&mZ;QYkFfh%<8I^8VBh3+w@T{pw*}up|#+6iAliI=l4~ zw<_M3IQ52vwv0F>Uo$#MmFV-3djWpZKFOB+(I5zq&?GTdQvsc6*vwD;VAH3hIvgC1?^g zV5Fcp-sfwID;4gm-sEiNT)!~lfq?wda)EW|i+PV}AT-2iI@Qr?`h3v+K`ST47|}NN zsm~`6DwMmGG-|nT=!AWg^^}lem88XBst~~M&A|6=V@_qB<%F|+gE=f$nH$$iJ1FLO z>rue3ij02j*HeF>8`ZC4*;fl0Udb#7Os=K?5Y1exP-v46GQEv`a{>=dR=VZQ8=Rj4 zs39hAqXmiQKfHV|WeITK$3u=)-h9GF&mD-!_#*5F>LR;^?$sEUXWFn&C^Lqna zqM+Vr0`F{*x^C%n)2smur0#QKhP5#FT?ggug;WVOqaEiJGt|nGBbTK-Nz#D9GZfQ;Of}7O?pe zqAy|aceN-x!X@saGmP%bxq~r+%vOl{l0^;Ari>?3@3R7#_J@^j%*0TY zN(@361a7`pp=e{cVC9D2FaQC~+>>KF{J5MV`E0Q8yeKbRB0C(3deH&5pw%ed*RrQ@ z0yAgv_PGL$WT$w*zFf9BY2QGPX%whK0@6?>;rUuI4*)K|)q~c97tJL5HpZABQ`@gK zz&8kiMH%F#p`7Bi-%b7PRpRYs{^||w=IgQ>9O`4mT*31~7Z3%qEdK>K*hg6>}D^cM=)-L9+n*AOomT;Xste>$?w4@O48e zU|-{H9tgmFgo@@z_&%U{h&Z84F>EN6cZm*V)BdvIJc0+5otTgYcX};yF+QhdIVJ`Myj_# zXp?PKSS?n=ZONaKwUk^LEo)!qpNzqu+A+szVknQ8`WNy~GWSA_iG!Q%DYlwZ)?|UM zj!LUtOLAUZ2T{hG^J>s;W)zURwzGxY(TKHX)LKa+%xa1IJpQkauK8NyR8m{PCY%Mx z#6rNyt(~RfYBUHsedNN^rI;OmfTQA%cMx|`I zR9=|Q#9D3*tsL&bxsp-1mXuG_YTC+nL)hw`)>=FV&G$|^`1vlZ9K$7fHVO+3az83S zSvN`nZJ&F){Y`CMFkotW2#ofRADy5FLAJ(iPGD=ny36Aa2c0ijpz+z>zrWALi|Yml zWdi)3$3a;dud?8O1%6o917_g8uF$We(9`EG0)PmlrSBv&&A@OcG*8*<+VeN!t4IM(ZY zLoTlWPZM#`&&n;ob{)jh;QEos26?13w1d$uEmdx&>Lpc|b)EDiF+W?Ym4ps?Wr&75 zusOM|s_hgjsQZJAqTYE4`%*+tDn0xI*}J=%#N;34iH~rQN+CSJQ&-!Za1K6#Mpi61s)5)Ot zHmj^H&6or8r?>Lb?-wFGD*`SK&Hlu8X*z;cN>g68`HHZZA%zs4c)f>rZ3-TQ>RJU3 z(XOk%JL;V0u8M?EJQA!@Fu{_Y&f(5{>TqG?Gq7%vGn6#xOvPw+WldzZXd){_i;OF% zH4?l%*ArUb+md6FgxO)kWZA18|8k7sH16csmAIUUqhy5s_&1WvlLP4W%wh1%Wp!Zo38w%3!tOsfA9%`gI z_;D$v`r4nQ%Pf>R6LOc36kBu`f5;L|$Y!mlz7P|#7R=YPwJpcj^@k@w6+Tl~1i5=_ zia!VU|4c$Jt{)EC>ncm>KLO@jWg3MgZ-?1Z`q~KABOx0n!;|OD|99YPK}Jx2?y8_h zlO(j}?`&M43g}Z^{a|HNJyPu&&zsClhOz4{4D99}BZw&37pzJ=LsDWtr>2z{qRco` zc2X}tbcSko4457@(ZKXY+_*Uo#B-QBe<)T6dxsl5 z!(XRz^9;X826{t?yi2Bz4tArtxT$Q>jf&D~c+*sJ6X2#kz^{=5)muk(4=$HvR)H;vv= z8_ls6qvH{+H>q%#&v+Im(GF}^U42l+>)+saH%#NYbQ8NLs%@$rlYTe4bR@WZNQI|H z=^URxb0-~;j!nWw*S(_YLXHc?A?Z~#=`w~%YFPuhBhpUU(7pnEWOLzU< zXp`=V;&zZ--Y!O?hU zlQw^HTLP28)zl@Xtznd7$Zix_J5+@VKa)Jg>+rb!K1B^`rOk3DX0o41m3+tRYuhd; zJ(SzTWGXR$A)-E%ZC#|2Ck=&ZtlF91aG*b;xpS)i*U|w@c8C0zUtlmQXYDCF%a@p5 zO_@nm{edbhyoi<(RAUDBLISm7%w z+SU3tUQ}#`lyzFD=Vq^V6n5pz*hQTY`BCyzaz_ITAyJ&&Co&#lo9xAf+cDJUoHsuU zkiyhY74%zC2ZK1ar1SI-nNh(>pOAbxRjGTYv_hdX$7#Dvr%M8IzUd2N`V*749bd?^ zRq4Jk(ZvY&boOZMC&0w3@B!Hg%Iw?YcHJe{mIRs$r-N+8(P&BsU#|A03hMlO(_AUk zf6K;t#*{CSUT>EZ9}bysMSYAp?u*g&Y0iabgEix4#HN7_%$?7fZJ2+zFu0FmGLz`} zWRVvevCwiN_XDYPIX6qlToc}n<}xVjeTXVIXSG$o*ENG(AS;g@xmSFRd+^0_m|=)z zHq{&B6LK)S?8g6_^yN-^wnwC;ZUwgf^2DlK;Y48)k*Nsr(=smIWvrK~2!}Cjil%kQ zHL-I9_Vb`2&I`{CUdRwv%Ta3Bbarl)%KV9KAN^7FFkY7V%L$Hzdv}jiy*Bca&4w6A zG)P3C$Q!W>2Rq+p8Z(tg zG$X>YX#MM@$({Bqfy3`TXlJaJ`et3WB!DzePukiw>!!G~o#qu^hhXjrP>@k4EbsOd zp0#Prv;K2Dq0#S!40X@IVI;jgc#~3__P3fE`rKuuL5en(RIYZ5Q+*`a-j!vrm?2J= zJN_3>`jc9R3}t8v{h52~Rp3p!?rvL-`;Vx+Nb6nz#j54Pp0yI32lX-U(ul|Qw>}j& zT$IHXM|bwBzl$uuK($Sw;!GXkQI#P4&jBy)m@L0?*{n^6)|}Z_|C<{Vmks*iQ@r~` zh`4er0|cA}?S&zpP1=99WGRC$f9kB1F`Q8cK@Cd<+{BaAv94p6(TjDH0#&VjFp_!6 zn&QiR_8#r1Iz^Q}(v%{|BHJqx#Y5HH3wXJL)2rWJ6@IjZ-u>b5P#s_zS3EJYo(d}U zT=0+d`1_P1G*A4#JemNH*y>#6G!1-Sb6>3uSSjSNJ|AB!#~m8^s%ctNj-Ds0b4+8@ z0XP|+?F0Go+0hB!kaF(wxtp>gaVw!xuFE4R77%RZ2V^i}@;vG&pfQ&TG_=9o$m!!I zc=iy={w4%2rc8XZpQ2=dXBTwsf%={oOZ@<#U++tj!`7!)r)9~B3*_966AIBVW zC~j^jSxV7vH68NFrbqV<1(9U-{#+?9-%UkJfTHqw_j>TtyPARzZ3(T7srHnt`5Blw z-vM>QkBOM6$Ekq4p#1CXeWpSb;GHM;KC=fSo2}&Jgxm%LZzIYX#v&QwYYpbPkg)hF z*`c9x;yg>tE*sCf{a=obuS=j$iW1DW}-=G_sRLd=;KrU`bVACiTF zuBRDpvmd+6$QRmm@4hfG45{%Fb7@K5GJeL}%NOraw%KsOLYLGDTNccwT_R7gLv@SA zF|6eW4x44>H)M~22)TV{eyNSEY=#eb8gXfL)RLKNj<;XYOAId%G?Rpn?X z7as#{Y|WSJTyYVoPDR_TJxCfHH%BZ*GV$!AkkjlRG~afi$vL(0P!OVV2K@cML(N??caTY!HG*M^_yuCF#EJ>Oi;6@QgdmcaQtM%&FTZ2r5`Ve|YvX!tEe zy#D|AB7GJF2`Ewbu{j3Wqke#|%2xSDjM;g8*kv_` zMLqQ?{EQcYt(!@E#c=q;``b6&J`P{91LtO!TE~TZ>=?@Z_D!EpMBDti_<254KA5(@=kv&IkR-m|JdT@|1)dcF+W8ZW!fXbA@@_Ebz| z$v>|@jmZ-<$cr6F< zOZ*meb>N<43Kg4W|D6rowH*yq6TJ#RjHcveZ{50F7ZDLi$V!#r;-BhcH7d{ad9bv| zt=S9y40EM2I+Ek0$zief-_|e*M&ywV6T^LFrN=?)?7%e@7?Rnf5nWT5SYG>3;@Ph- zi;DS~p)7z=c}ME%=Z{ZreYt!TD2TK(66kU%>Je13!HA>%vQxwj4%&rn!Bm}L)+Vs5hG>hy|Ij2#?&l(X*NDg=o{P`@a3ab zst0NR2`tMZw?)$=ULPaI%w?lfbWBw8l}e6Rt&;R^Ri#%yr-$UyCJ_oomLf9d45J>> zq8D>?9#>I{{2(#rxKQE~`P>+6RrP(NG8-p}8n(;A&nIJEnYdf<1SZEFTTw;}ai6LN zflXTUp%js{{3&;23D zOPSoo#a3N1eTX4*Jo`S~$7b%CQiBHLGWE#havM7*y){>T=U4G} zEYk7IOON;NP(3B^|I(qd#p+SmcP9TrR31asJk;uZF>Yj!wI-fo-Uu=UK~larF}2?- z_d=6f{jwTLj`LL5*)OiGV=^BsRc9ozVuZ{oH3cVrdDHV-^kT@cbV$0Q$0RiVIo{Bf z*G7u;N0r%F%bJK*nQ0|VzjuKp@79sv@F@$qrIs}RcNm3h#v#$Pxs3K{)b_uZ zsfMYd%i2BqJ=u9q&LnAi(-RXDoqEREv;!uFyTQ49ia31e<%Fqt(tDQ3zjln|h)z7x ztBd+&asNfEk*I$gywbkQ-uz?mY($iTkZUXlYDzf2XuP7GQGql#>E7X@K0kW0W#V52 z{oi$YvfCb(zcGffx5e|@V~2Nw&UdL z{3=@Zspa*zDa+UQf~P#w;{OQRSDA<{2=RzVXXjM_)`yAN4+_6Oli*UMmoRDsr64KV zw4G8=vn}gQ!JQ++-`P%}kY)dV6DAzvG}DnlLL_pB;|5*H;I5W}^j`E8ZTygS31NRC zbrtWdI*t`u%0Ju)J~$J;kFx>pqWs(%mFvycW*ZvE#+HH$j34-$djUgrcZP3RlN%8jaz?qVaTc{gpvwj}2KwBwdBl2Gu`c)bRjWq*YqeEE_9g zWltJL?pNi4dPLZtszpM(;N;uH(WzN#T*u#If+$DEy3Gffnjvny1{2-Eq^nR|BHdjs z#huD|MjEtiBW0jI|)y;Qnm(;`i@x&HaD7ttn{> z273&stu@C~fIn1`$!aP^^1R59kH>p3ZdOMti^NPg_O{k^L5Lb|TlEJU>d8^Rko;bo zbuTi%KPfuB)GvSEyNSV+?WyG@&>z5ALWe$8_22ori@m4?~;ap5sEG} zdHBl4pF4Avz3f+7{Q%LE9k_=F4Nt38b8roNh9X$cm)AV!5^Be_QpLOAC7?%flkl*y z)Hp7vQ6UStZ+S?*`~Wq3;p0BGgwJd2qKnIIaHPH~)?nW3K6V8K9y!AQ2*89!rqW>l zp}Id66Lgy5C0cs^5My)w+Bl&}y`V9qa1`U}agRDQnmZiVy@ortAibJDD<|WD<;RZp zx5hrDRO0sw0SCo`@CSI^VHG)o4vT~=^X0k#Lf@CL+*Qu|IlaJIkN+)_EPC!rP7HIO z+ZSZS{g$REvG>+tqoW2}$J%?xAc9!VpY8`7&ANW;vru=?!ntDzGhIhcoAUwd1auZ! z&wr2Xmh#tX%}XP%Px2z!lpcJV5ajch24st^FH&T@9$u)Bum{&5g27xTG zNQr5a5&Q@5J@Kwr=6F5q+*5UVCw!|&l543z&tv5YwqCrUW$FHZ6AGlRQ-aCm)sSyF|Z@kTRF(J6)w~6|4S+8_43%_0l2H% ze)S<@_bDNVfhCcns6W6+r8s;FeKZzFB2qybDCp#9uX`~fCw!rxrV+`F;k17=u1#ZM ziGIsH8|7{|`P32p!JKRj^gYWEi;tC{B|MZUWTcyzyNkEe=p`X$SX1VHqfGbsY-IoZ z{}p${?AS#NTFBVPg`Bnu`U`1VDKImG=J!W%e?{#CHqf71UW*WPO$ZSuk4+AnwRS(eD&Dir19M$%sUiv^E$KGuVR8t&0qJ-?4J zEe|kqbxM+xHSRNy$!$!40U-bW<@l-3ey!qIKxyT$iDekQ$QMp0yy*ea&M1<;0!GeH zcMY-%Z?tjHi4B)-EM}M*04GoXLd}I2>mgbEBiqnc|Li)K7fAvoF#!1|rqueOPda}i zl7awZ+hqKyR*+;=Me~`a%}zaUf1VB2_>uTisB+pv34(BjD{f?xM*>ozQRyax>3UPi zD0wxk8H}NRENzP--_+h#s1b{ob$G(Xp{CusI@L4emsuy$N9nKm($!Ht*D9AzQ)U)u znT}}qKP2K8q-SQ8`1QE-12hclw*}jHaHsd_#%E-rg!~;FTE3M&I%}L!r8qCgsH8FS zzZnr~)42oVHovzdI>~@P^y$%%>eJfOY>tx+bClM!9HX?q3pP zfjyCY{6PB47CFGp`BtOcp9qXm#QPjAk&o53*%wfE)o)edL$gEMbp|Rt2%lwgqQdl| zw7x3*vw3M^0NO1SCh<7Y+=gHWkj%KCCN{?t6QloM&T#n*fChES;*XU#VBs z_n>!eB_vmdRpnQDrR`3ccT>zN(i{0^@2C6cOT0k+X>I=3{qpjLxY!|st9>`Q%n*r} zBO#kt)crtbtV{m0#)42|t_CcJ1zAeyU0ZgAjj;xy z!JEC3rEUBLmb^}3+Gu<{nu33Opn-9osF1eZmCj;LoiHNZn#9Npe)dNOT}zFMr~sY(`4mzv?pzw%l(9ccjF@i5Qt_N*HGV%| zf+z|)6wjV=j|Ch*M9M@fT7LRH<`A*}bfPm-{nLJXq<}1;!Y^1vkZ&2rXTFZ{*6WvH zqy1$7!KnloGBx9d87@ZMQN7BhF5S!5X&zs56guaOg*>QT&T5Bj*Ju!YC$}qhWgI$? z5BhXiTv1~zeRR59Y(QjZ&Wj>Yqpu#(L}wQt20Tz5`!$A-%!g~Us&tk;COz`hF_t`qW zseH1UE6hs$m&0~D5{P+{-GQiAXo+YTItD7*HVy#G6L9Vpmu+y%Dj{^Kpy-sPpszKV zDoRT|mq)|1_4dm96nioorqvOWO!YZCs$Q~PNo`SO(fRib>~&39u1?=Xv}C*YQ)3D8%of86&@7h{- z>D0mM0o@_2%gMjgnQ|BJwyJ?fZfmrq3z>DW11c16Y5 zSHhOvT6-sz7NAWdWnxuvgrFL&Rl#^|)av2WdbUShwOfV;^NRsD%1#luspuf`bWTp9 zUTv}3WH&1iK3rSPJ^$i@?Z%_15`T>9&PJAng%f?oIAM<>VF@>krJTEBl{{+muTTA7 zV%G*wJ|4?TmHeC66Y5Uwa7n8VGJ}wvX*v-Prn>vi z7rzu?(Ko_buOPkgj>IPZ%_2?x$5($gM8yj%LNCiEJ|Tb2Y}F==?AK6z7`T`+c9&`i zT3k{+lIs(hg%QIJD(aNYai0;R0`ULg>qU>2P|AO!aD;GOfi1NWOBapD9m(y^dEvPq zgG{0>awY!_?2uz>VCdG~xqn-1qgBT`AyGeTlsNnqT&PhKVVp#1wKLXuvC!_QBdrPv zCg_K1P4q4#rXb;#h4UgTW=^QWh8%pT*U!9^(w5C^d)@6X6hX0(=}qSLBUwe&-W;X{gH!&0xVcM%B< zE-62_x!gn07~x}u)b@5YVB%4I_5(N!MQk>=zTQ|4R+>tZMRQ=w=KgqP6(!u`vp{`n)#*7P1@&WmK zMMNLbMC5L@J4b0ram|K-qLr)u%|>NiUF_U7O1%D_!f5UYk&_=Q%hDrCm_D;nJ3q2~ z5}6@V_qHJ6vMX3if~*U`J`InMWTU2L9N`*zWy88C_bP+I;em)4Z%Hg3Wenu>T1USBhaWe^I*TFxGFuZ|jTNXpt(k)EMbj&2`a zo16QVJl}^UAAvM%i}uuLsF9?yb^ZC5oRrfGc_od&xVD4bOy>pibA?8bQGJKWzn*V# zEvEmt&yhXfS#obnrVA76CjrV z>E$b3lqKBIby_{ul-zf4HbS*50Al&{xvbxdF;zoc84Joi%Hh*lR|2VX1cVz z()#OzRo+uJlrB{D@xz*w>=$Tv@v$n2zb1Y_85L(J20_x#7F=E0rwyjp`r?#YMN%(q z2B$Dsc*W|`N>R!{)hlUK8kWWV;}X%I2N~0H|KPDRcL%f%Ns$uL!L)>olB>y-C!wNB z(G@U5Ws3IYe~g>!IIr|O;jU^IQi_7@`*1WhVTY+N9f&^;fy8|^gBQ-`3p5-wIXiU) z@O@yp^C|yK(XXu|8zVFtp1^lTE!HmmFok5kCB9x{K1>9qDon0&1Qfl#-f<*RcGI)z|K^I$p0xcAS?0aS<1liI&?ds-9B5jq5Ve8UI+j z_j*yHP;}e>u1Af*B45BY89MF`8CQ9-B$#q}VeRX6QWXoxBQk6@G*Q^=NnIIx?f#u@ zJi!+B-8t@U)d@`N5BV`w|9wpj86GLo76^oYn8@NVltqTv z9vNk?+2{<7;3twK|0H0y;S2nwZiB$mh%Mu{C7uB6y-#prRYY~r`<8%SuQ2v_-^f`!D;!CtIQbYxj86Kik-j_b zP+;vKV~RQ<7SnSmx_txoaBf<0;*7S*5w0pJg&j}PQ2!JG+cC26gKpv>*Y_d2g1Ys4 z#s;@G*d^)yLdsu~y#%~|Kvc<-@y>^~o%&E(8yt<0Q#xe0HBH%#=!Yz9u~ELDx6^F% zZ|gQcJQez3l{4V1adHLjEToYY1nKHW)fGM&QBU}BB9)g|+I>hAryNS)FEyR$d^aJv zcjv9btiAG~1inB)_zrjsCf@c`PIQww0`0b$g8?w+v48vrWMt&;E z71+xELZpD|ou@f@pw^a4%>msMm{ghBpj9~a8mqhdR`)u*9jqwsc_zu9=bhhutJaW- z!oO}+A9$hPaqfBOWn#|S_bS4X?4=>09RNQ5YH@@A-1W6vZ_9r(m(}&3P2ZKA=he5% zR22@f%&YagK*E_)M3*aX_-A2)1JVahq;>VC;1qQCux6B48uh7t!~Hl2FpPh z;1S(_Q=}pFe@T{)!o$Lj!`$NfhsgKp(n2n)-ORGkE$kORIx!Go9 z-$<=`9ZMt;lks_sOr4Yc?-j$jtGmQzRX5CLFf!xZCkp)<(&8w;qVVW^Sa_ByElg6X z3favN-vpq*lW6j%wS0F~6oV)hxK`!P$G1mhm;EXjj*z66X&*P)M~mqV!5+$!to<;u z?{8*@*OW1rri?HWZLAe#9d5*?gSb=ZO&CD+51+Sr{zg&hvDO}!U(*+ZGQt{zJUYO9 z2 z?rh@7+pTL6FzhuP~G3xIZ|8Nn;L+Y$+%`ZR!NkxS5( z7>3_q^WiUnx8ERfFw1SVfd!4hY(6zd&~xH=hr}JTl0MW}Jj;LXF7se66)N588dTQ% z%I*KcQoybuViy&7PY=nYzVlR;Nd7k?kzO~u!eEmBqH=p<5fotxDmR!o3^;##IJ&ca za_FO$c!~MqIiE~0=D+>5SQPR)I@W9EFs9KB#k*b5FlRNv_)fYca=DPd)vvwFaB%lh zW;6}PXsU#XR?vmTcRYhDN15_$M##O2@}6e&emi?W_~rE&>7ACnYenWPa|!S!XBI*a zZ)f?i1%>Dk=Y2#JrV`^~jV9!2`yjz4fe+@~ruZ|j7s?(pocAZsG)ul>|Fcj{U5+QM zP!DX2YBYC2#@WDS?(d_Fv^?*jPaxUfs*$X0!$K2_GzvMNG3h~FtW45;KefNQxuDd3 zzdTvnHZ~+qVlHk=AJVaN?SEiWqFPiW#|E3ax6Ri$z z7P9~{1mw7OfO}5}Z8O2?s!U_DgKaF)v=lS!rJ;)OjVe*bcKUQq=0P+~SMKun?;LZz zBs9P0Qc7SL!Bi6VyxEPqt4j=F5x&D)YnxptMw{1m-Q_zSp@oy08JWU!ZY(yr>SYUv zq0*u4>juQ#M0U#XZ`EZDiyRJ)r`TUaJPTc|q^>&514%VjJ`d&+)kk=P8_mzSw+z!a zMaV&;7MtuMy;qrs_Y&XgUBp^H16qN&LZ=IMLd;#t;;%l1WoK#o2P-?q9l&azRi6fE z@Z0fW=V!O%v#)FuTBAGg5pfC@>1}<7G7sR%h7=O?llk@cb%^5&(aZLGgy)l}dTgOa z`kO>wt^Yt>5@&+Xj1U1` zPcJeq%$DBGO?UwH$%5`tVcV6p$vS2Bi*qeD@(M+3xXU9?3uQz&u){!iLuu!r7qI`f zumCWb=?wrADv8FO&}OnANVtEEvRU*xF}u#HU?G|(vd!LD~dJ}eQj^PrR>Pp`;=2nIUXsCq(xTA;o;e=Ry)Fx zVQ#$i}Dt|MK21%;ev1-5~G&FMlDQxpjx&RN>Gva*vY} zo_d@#t69ymg%-~KSOLXVvVqJ)ZJ9)ojR>_7y;v)}H{yi;v2eB+O=IqlAT9@|UZ*qY z8xo2)Ado1M(b$$V@qQST-Dwz~(SwrJHxez(kuZYAYhb z-GXks5G+8>u0OXx$%3I?Y`ztVzTRP(EYY&!3Yy)pqYr%!DuX?Y6%0$)y!m3SSz;ZnGtGb^m4KnfM$ z+9Z?pDN;GvKBC|Q#=(xGS67-16MAZcfzSREY@tw}grx{nFBq+Zv12>3(je4<8$%OV z3`ei#k4M#b)|}L#@BA=@&DtT&iMb+Qsa&;r-~zc!tQ7oaVp; zMc;+ZN~vw@gFNsIrqq~fBmj~av(cdPIzojHXWtEpFcxRJ#9%&?B64| ze?`8twHwDiT24P}QQDoc?9%hvwRcrU2lmSNz8!Mw#+h>Ai;tIPHS0h6a;SIkDW=0i ze4te}8M1yEQ415!*3yoCTc{q<;Owj!aHI%oPG7+H9nx#AGF$;<1(xWF}k!C6uA z$pAu%VO;TsJ8nUh3unsc&4rs)Qe?s;XAByX&54W@gl4HjfNO^)&4&ULPbQ96@&<>r zDAenRXBVJWf1TF#;J~X0-~dn#b}PWcU@Qa{HY($Ukp+lwAA^uaP%A zYsi=0_JP;Q`?g&x@A}K9$jhGfGjhq+7s&I9p7>fCyyc2d%MBa)qON@VYhuU##S32~ zFFx)W@#O;JRnTb2Z2G=X_7kcoB&<_;O< z?hxfdXn}bxU;>QBQOUy|4-Z?+_CnwR;LB3Onu4lCp}P@s4v{P(z!J2f62+JwIW@XW z=(;Z4L{UQoG6KwyL`jzlF*9Aip8mx=65fN7AIfxKV<6&7P?xVa*}eK`bXQx%pJx!_yEIXIqKp-^HEP7Av#JZU&K#0}~TODs;96Y&Cm`8e0RvIW^4 z2L$#9uR|-33wM4f`*y9!>iS4~g1h978$KtW`@+p~_r}53WRfTQn_=T`wrQk9DMy=x#QYxvSXqwYv-!| z!_hLd_9mzJ+_n9(YsX!3$BwJt3UL}#-l6Bw!3frPoXrJ3z)2py zu-LmxbJjqG4tq8T5B5ce=JFL?nc$#N9Tip_yI>Zf*=b251ZnFJBk9`|3m7q4ickM0_v%mDCNY$_X^x2NDdjTKN+Pv#bdrP9mCC(5;J zZS6b85BV3VS1o8uNc@}uFGWUkxM_DM$3gv$ZooVIz`BLO%;yCtJ> zN`Xed3!A&D9x$#L)C(*K;4Ur>XMACz+aL7hz8(8yX%2NMBSYAa%ep)XasZtHexIP9 zg1w$~NNDmsE5EsANoNWC2d0r0U<3sRJV}b^>Pydh0twg_00#k*IWM@*^-7`c^Vw7@ zX<_d&e#D3CyK4^Uo|AFYN9hs#3CfWeIWC2%5OGsbVuZbw=ph)(J7CVP$n-2H8-(wHOe^i-RUeuO87l4V7d1+_iMIW7-hwq8r5wZ{^Gt4|#` zCS5w*2ArDJtY#fL3&X!4EPOr(QNbozn(LBNkMN_62hi#FA*?I7-@?8RUV|RI3YB%C zY6b8@<;kgy%t3-cUZ8oHX=SA-DZnr@cYOhX5t@V&`(~h(J#5xA6mV>##$+=#_!)+g z^^FNn(Bto+4Z#2z-P83%@avpOmk|LCmK@{pM8O2E5Fux`ptb|eX?4t*BA)6+MID)9 za93GWl0z`z(m5W02ev~Ob}}#5z)AmiGMduvKGSn4oPr<*#R_Jk;9rGVEA+qe%ppsJ z{y(SVhsU`Aq&I^>wLxo1X%%QG*03-$ln^J%f^q~(qU6ZF7W#G`1sN!%vLsjsPx1?d z&sOddG|9Ly(G4pq&|!EU>gUX+`uZFX1`d4?nF*~Qr~OE2p_ivmAplx{+T0w3DR7xT z+@UOqVOpWWUJdsQ&Z?rn6&RU8 zY6id$qe_05a5mOLhl@Io3mS=%b79zwQB_N(R>6b%+GlyooalLAfQjqmQX=hA8$ifD z!KMrK2V~`F6uDJS8;-65$#C6iBuM=303Ga1z8**v2 zJ4`SJ5W9$dE=sz|V>YT}3Q0u|!i=sO3NA2nRB%C=(t(nrge!5uIj~kQl6a)&rpN2F zP^xs;w<{DNBhO{oah4UF00)a~>9wV>&MLL+H$TuXbEcm+mD$+kteT77ADroZh&*hN z8T1S+uGP^p>Y833GtVBO1IW#4RKCmvN-yL=oF@UILLvl zbmJDf`l4o87O8BoJ_cE%C%smW@g37yrZvh~PYkBG4r$hE{&dYqiuZ++m7(z-21_)l)>V|Z|XzTyxbu@|CePUi0{CZsl=7BaGL8bCZ zLO}#(*Wefew{|E7r+O~2GQGJ7ElH8S(nc^ms4wBvQ7KRj<^x%t_hg}4$$Y2KK3OG; zbFh!fWIEn2Wjx{tK<<>BVaO)R;wpfiPq7FF`et#)awo(YolYca>PnVNvzpbcW*rg> zz%iMix}`lWeTZLZDZs$pM~RLD8$8|Puot$w7P#xcht-42h3iRJ>9I4r4o~ry*XBD!kH24=OCS+zr#Ac&L4 zY!_ynz`o|Rw}d+a6h(guAdC>mN;ht(Rq$t==|V_kj@hwNmJ}e+Q=5#(5~JCN+$&`_ z?La|Fk)Yjyg~*80-I$+h^?e0I3ziHNK;gR4?)7D3Wu%{zkkt+5911mt8!=0baL4F& ztbQ6R0Ck{tRGK`?X1D}eQbnM?l;->F&9R-QOlchx;(8&l<=+@46sSx_IrF+qJt>^~ zVkQ{uQc^Q0#?#CCY9ifkAV(}JfNkg-N7AfjHLF?w?hB;gsKt&%&O{P@$VQGJ)P0!g zMTHGWwNJ~4NItF#kL?&dkXL>JkpR;`;Ff`w5)9T+0rTjxP{?dF(fzSa7o-`orx+IO zaX$rPC?Jqi64O(Vgh;T`r+U9d{w3@OCDT+(VpE#DVpeIYz=JYd3wBI;1m7HSxiKTg z1HGpMy4iNRXuzd3(Zz=E29bABMWMwt<7}lEy;Da;0XD;?b z+*}3VuClEla%i_S9*<;k`3U+5=DB{3qrEb{wj^`e!#XG^!4NWT0ZhUNb1kw%-^&P2 zSyh)YDXb`|d|X`A8z-j9#Fu~uWJOiEFaQzEqn&Z;d8{V^Cg*C2uM=7% zJ%9?bNhDl)iq7-8@uLn}5Ohj{*QVtT8Cag=jIalyPo`R$EPPt$x7$i5$5Vn)x93vA zKbzHASqO^+6O4tdDK8@}305p=&a^ZMQ3iYtA;h4`Ej$ku)r&kPTN|ye(K5>M%?ZPS zO_cr`|Hn8NC}k9UA@fA3v}ANkz&}SB5w7_rDl`SmWE#t)fU&P*GjND@@f=etV)$9F zCsIIN)Gs>86E-&63iJRhYj1sHRpH^2j@+zfHS3YEXs4@ygCzTSqOA>(3lM}3%p@v4 zki)mOtTOI#NV}h(3(-%Zv3lke78j)~tJe-W--UgWeX;_Tx|nQiuB!l5T2g(5P+qiu z5FV-6k`EzW}_amoZ>08<$l{ zQ)F8C5wpMmh!8G>kmI9g=2TRqpd^W#5c;LeEf16}CJI8192nZ*TF|!WXPY*zXthLV z52d=8?mjJ06~p@-|4YLn^9`X1O&p^i&sj44c+ffm+d2GwC}`P8gtHS3YK%%fIlGJ(A? z)fPt^DaDnUqt7WzYB?#ROWGmww>*aDLLMI>;>ri<$*34)V!dmTQ#cN4W6%u5x=%0Y z-k8lYR`c4dhzYR)bH6aT@O2-@JeXfqW@K9xkQ)~4-+)D+e}m!LmNFkm;vGykCpOE3 zDQPmU(4(71w^=G)>G;9%&{FaQP`dVK8P)WOGcSF zgia4LWnL)b^+Zc^7=X4^O(;_6ok)iaWmo#&G%gud7U^psZWwLMI4c0%f56FZ_*N8T zbNGxDkCIY>Ow@?ZiPzW%y92r2Og zFv0A%14W^-EHG6BN5(v!0#d51uuro^{nBzWrb2smV6m7FgUl3_L9kPZV-PCYt~_a0 zvzpbcLt^2{TKDq8p5c{!Iqc12CJD+1v`KO#d%&YLHf7m-way&NPa1aOuI3$Z1smf-F zXr}~0f*dUqjB1z-F+?(nSroV4uyaeHpC~hc25pQmGXk50>ZhReyjx*=jcZj8JjQ6;3AmhHWLi~hLNKl_C5u` zHs-{%6@zgnRO6tmMjMD@%uvfiKsXiZ3Va|RQ+RbV?e}bW7U{!2gAPm1YrzDO`uVzx zBW94nau^zIfnQR(h##Pzh0PkJI1HVQd`OH`|cxi_lG@gaIz(lsFxL6AW;>0WFnKQf$#lqvW|H%5LD`j`_E+ zvZQ4U3FBB8uHIOOls&qC5DsNXSTfR)nA6;u1h9MxJ+eFeXFa?6IABqQrF4d^NRNk- zsF^~dJcu%*gTZFrmx6O`CC{lar;#$Jcmqhuco%tirNWjrO5MXc1EpEbYSzE!LiMS9 z7!S&xf(a^15a4C&mb?)J5%)gCJwb@UhyL1A5I@KKE%wS#%Y`s?z#d7`;#pEW-m|iv zOS^}3dXMEq3^YTS*K66(GTve$!In%WiQ-cN#^tcL0?O8Iw+SW(ff1WNQht*ZWCByb z^{}#I*;QOsNjZ3_%xXGK*}rLdW~T(3MF<;E5@YEPibTB_3?NekLNi?Rvcd{{^X((N zf)?iHaoB4)L|P6M!-SVI411aNjY^gp0Y=E&YDJlTPD+<{ptNXt&L=ApT1yd0D(i7& zw71f8Pqg%>jlnLh==Wt1P(Nm%6zdoaiW=6LT={1IHvVA@NF<6(LygJlI7A zb}<48WJ;x9M>lEox6ptZw49oe2qln9eWd+P6@ddb?I7ESeF)Ta*ubID2jhVxKFhVY zwAhwhp9s7P?&>DqptK!Ggd0RPVL|qS8;PK?Hd2_YoNgHKIs~x6+Nq;A2gZ)nL;$!% z2rdwU3c{{EVY!lA;W3WXBU>e!X zf$!4R3PKM6l}ljr=wDcAJs3|g6)nGVJ}~@iGEF9vi8QNO&1%;7cIiGFgxZQx@Y3EB z2mwTCNNqrCsRb~rIDMu2c}}eaN{0|%!1@XMX2lebvNp{XR_;IwmKQBS#LexYy>yH; zIHgtY;1faER2+b20X~O5Ap(` zh!Pf1e#u-d08UZ(GA(k>KecKH^td#(hG_Z%fCyHn7l;dEJPI}bg`u`%iOQ`uGF>na z1|SNg-!asNd|cen5I@hz+|U#RC}G}gV!p=si!Rw*7*+Jl^0XOJGx1PX^ zy&nZX-=)#+By(uIJKyza=LDVzQHJZ)NxI;8`JKLwvEv)C%97NIdz*R%b%-3~}*#-q&a|3GfA*DHAdW&mG+A^yCwn6Wa4 z^|8ptv=FW236$F--MnYxF|CPMdV?e$AFP``)Nkl51xja7vjhu*-_&fRU;(9`Ug)Md z%eI0A5{z#*VS`5bOhEujzjUn3X_7Lj7z+;cHqTXYcFd1Za!VOw((l7qw_x82jXU(Y z!q6^)!IhW?!!)*n4lI-6bjBMM)d7{R>(!kp;h*tl2L=01ICsGN((*tKtW7AnuP~uq z8MVdvftpVB8-R!d3D0dU8)i@f_I!fUK?R`ehHN(CMniA`Y5r8Nabck?MV8TF9>d$1 z0H;-dD*UoK?TB^TBR1ilWgNV7oCdv`)vRV6DhuOBE~`l?Qs&u2ak&>`V@(wYVy&_v zs2F=_p1K@ZUK*Gs;BcgSvhIgoWM&p%RH#UyZ?>>7&<<2AYvY(n_fCb`SW8s)xyRS` zc!jAGeUu8V~^>`?me+K z2NPOO!E|tbuFrci(HrhDg+;;D6_)w{MmTSXMKZl^gj>$dwI%2Z?X408K`^$V-nY#P zSgn>h(mQTsGjHESaM6e{n$@gkeXrI-JM&ajNu%@?(fO~XEt!~A_k3s0niJ~b%pc7o|1qy1-iD6Ou~YRAE{R9QmsPG zEAXMLNMJATR0)L=j$;e~8!~`4G)3X4bS2T!gY4Y1Fh`sKu%pm&UI@wwhl{|qlws}rY)nMfbOW#alJ6@DLs~TH zP_n~&M&p7CztxqQWO2^E5p{Rk_)BJ}sg?v&%t`gS0ofg9teBA%Sl!gO78Y%*V)UmF zEUCvwbPE^~N`9~{>4b1_*FKiApl-_oDnV<()&>YjbBKdvT_|s&bP0(Ilz@=@LsE~M z73+CI$bs|3pK;!;%sCVBHT7v_y}^(vofNB}u6ujBAUkIP%ZU$O4RAH9S{5QPN=g=-nStzm5F<#HB z)op93YeF}7Y^>R|v|6VN-$E^&e^s0ZqXGv& zhnayfevzm2iEU>VN5|bc88U{w0GPX%7-5YHV*SUXx1Dl#nb5ovIHU6mMUhZyF;z`b0D1;TW%xa$yh~T2p@Wba!wS?B`nVfSi=)R zga)933(m3A?MghJ3a0?_uc=>r$A+X!eqKeHKRzH*AS#_ytF1_2)?;IiD>vCDN_d@4 z$Qvp8C6#cAkCoXegCl66Cl}SO8F~*nt3?GR7+jz|H=lW8X_^u^52_9E`C%7S4N=!{ z*wE|i>Ajhn(h{>@&1zP&nsq2FREc0r&MZdXvbm7n?$)G+MhpsE>*IoztUgAFknpbi z9a=V-wvQ-;3j?Oovxg5b)BSFKsV9jNB#6b)|BpGmY=g=Osf2+AJ_E;ZQPBvop3*na zKUi*HFpPt=nBaF9;{G(mK02jS8L z98f?4*fPBzusMN8qGxEtrw*Ji^1*z5VFjNmz$Q`l1Q>m&1zP&4vB@~V2(n$wDijgOQMGi9yWC}v8yb%3v;pEj>CkFnLe<${g5-b ztyaZ75<)e+7q3M%Eh|#924sh&Bpz|d7zy;D#|juCs}i^wDbq`pIJh_v?1(s9`C)4+ZplSdpqvQq$b#r;vtj!jnUpVdb20()AN z=peaocUvso!3n7nOUFdt2cCj3rA5!I$V@^%n!;+ydPo3T5F9i9_3LvKmK=k?r1kVI z#4r~`{-yPjP%q{Ac*edENtQb{4L(ya9*rxT6>ycJYVR#aQxPsrp2v6nevJW-%+tEFtjWO94>f!N9d)HM^G(xbRx_+Qt@p?bH6hVO-5>RT$;cc>}-Vdj3Uq_G9iJ1`{iL)I1BRBF+hQR)1-4Cazvp~eeQem})5$ou8G`wK zz*6aOhS6n*U<3;)Qd1WOA|-zg;Pdr79M8lQIIqQ?XJ(O(&9;%P(F)7X5mr*TWwb~s z%)1eba@Ey$$cFrDS0L^FW8@i^JXY5J{@rrj3XDDF-+l#c>7Vgzx#THF zi)`E=ANh>P>1Q7$@y%DsSGKz{zxB~_+@_h_^O>u^`8{b?vkuY4Vq0mEOCfqvg1)@5 zC~Gs>UWi0wn6Y~fT!M6uhbb?TG0@+F)n8<`fwT{FEBo_SNw9)JFHC%?M{sOT76yVK zIf3HIpsSU(Y=#o00t@DxpvBLj$5-~G=mxnqHgOD^(bJDE?ye1hLCas5TQ8Uhya2n0Z>kn=K|dcqD?UE4d24bnuQE{%uH?$P=@0>UgMS4 z18Xa5>UUvhrN`5=Y+=@mw)~zcVrW@X@Js7;9F`9cs{HT;;g}@Oq_?m>acN}*=7|Al zWPG8g1(u6)DtcQO){`QjPZuW5%hDLx;kgvJo_MZ2&v*gLA zEz4Xllu*8%1>ibUp7V;A$~nt$a?#^H^9SW|r*4)(Apb_Iqvvp%oc;V4$qUZeB3m{O z^zZpV`tysjIN#NOUv^K;Iy5sILjgx+(`TR7P%K-HMDY_}GeTmA{g4 zTRzi5X5*5k>U3oS4(aFmIpx;{-6JyH=tr|k+HDrO^njIgd;OYmm08OlOAJ(5=HZ1!DrDe=%HsA>)}jXt)GhU%cF-wUh66WTwqX zlv>ft!aV}JgWv<;@71ggiPCiaa9g~up|W3?OBS`6?<0GiEa!gzsnWjlFXi2T`(e5K zPWdja$I7{vTrBg~{GGh-Z}!Sf>+)}~6u4TClZ&1?kQ?6cI(gkc*`MCN{ov2fk~5`Q z&HCOhIQE=z#u>7(uppoR{O4u={{0Wj_5I0|9IEm#H{=uJnyBO?l#Al$@AW0GQ2OB6O!M`gGdi#=pKc(sQxq9A?SZC#5Ve zM&_M3jR~M=>fuX)oj#ZpnuX(WO2yt0n>zaYoaMq^uadqV92QU`J^#|5nUxbudM>>U z3k9pGHvPgXBPiofL46^!$CuT{ESH;8fKtv}HX1P~Hyom`g&Zfgu71Mif+?&t-<$YQ%QCre99GDwFqoMsPk(ss;jUC(8{XETBN z-3>=WnIs;M-^S#8f*HJA&v`|r^>x~FrhbRXn8Wi*xAb%S;%6H&Ot#48!G7604D9&( z_exSc=%XLs2usZV`MHa7nC#rKC=Cltf9_$25c29F1wT>rsp<1ROF2pqF>Lb(iuwqxg8vZ3?^??lQd=4$obzBYr=L!PoLqSM^i-!jx1rf@)5KB_(g+*Gnbm71bhJ)c`2rDpP zTvUumiQpLs!AO?!wNN}R&IkOt!#EYsnungvdbK*ZX}Up9E8R3p)>z?W51cx3Q%iec z1;Z=XSEn2k$l-*qKkw?(z`TCRY1U$4)cS-zRfAY4A7WS!RC#@+v!~XHdOrRtdDmEn2Yr7o9$_oqj#b=;p)Cyrklu5teQbL6m58T~ReRHjsG{gLp zf>hkjC|KwX+Ek7Nx>LhwHy-Co-7>mUboD~k*AmjaLf_v~AQ>r0I1$tzC7t671)oHL zWI9UNs9T;7WygWc6pVTeT?K-*^;9OKijgUpnx5-;(nCbaG?%rRCtZ{XF>Ed{zn#_M{Vp353)gaKs5quETaL2qzN>0X&sEXj3xIA$Uq z7}Tt!y%VkrAf70&!7aqqavy*f_h)HY*mQLDxMTgisXlSH#ngz9z(Wa$Qsly>fed%A zNv?Q)R!(j`L4DBoQ#nl5Me;}Qxm2caJYQb= zkGo{@eXo+A{qbkXDZ1ldCCACBXIvn!xa-FOGKhu)rjejk_cK!i*-8-Kl_x|F`Ug{|TWh5b(qwTaX%H!8=NfosS zlZ}$eB8x{X$}EXl3Bz6onZclK(NT$#jdr&$%ZNZ3jkL{Qk<_eD#r$NWf+J3B6JcZu zNgCMmeYhY&cVG9gP;=~Sxeib_)e_;-{G6g)LcRTTJmE=ov_S`yMI!Qej7D9=2$O88 z<*s=j9o=7gEUvWFh`>nYeCSJM!bT${@t8db&|`BM zb#m{WE3(it6{X`CNYBA=SDz@*6YGR{ABL}?l+5U2?*&labU6%QKJ$l3kLxif$%*&8 zU4HjtW7(y2Ve;`$%e5!Xi@!;KHrJQd@h8e-Ha%I+`@lB&<%93J`dVpy>>&_k`BZt* zCBG?ezn^m5Bkz&#aBX^uocE%ikpKRQACS$mt|hrEl-;~uAr5PYyytiSRKBo%ua=|l zlk2WMN#-86MK&EN3S2P!yh}d$v0LuH&SQgX<(jLse0eYa;N$nd{wQG7T@Uko9(YgQ zvrR5N_~*PQX!JF})vWJ)z3W}?lB174S}wixQfBsDe);9{oh%wbOb~&Ui<3o?DrjLY z3tE6UEzJf{_ssd8LqO_wVW{B9+PHgY^S`udt z+fL)yDrAfOKzs$YuKu?$XkphUS(%~o24x>pgwcEQUB)7#T-QeG@F=E-AIt*h=Q?!H zu)3y41jVl*+@L9LA5IvVqNMprVbexg%y-P*2w>!v=->E!FC_3uk}7kVk>PZqIDu>& zqKnLuGZ%UR^R`?CKjxG{_KsOF-Td1jz{2D3u`6bkFSXZI29Scet)6d6mkVWHEsp{S z^uByF0WkuAYtw$iOxY7yP*)ij&J|Wsnd^{&F+K)ONW`}|Hv5I%9@tY`duPZO%d>=K zRR`k%Gnc{@05_N_SRw|C^BfNVcw*3A8vwZl-~;V$>-m^K1m}UF?p7E+f}|PivbO6Z z^7fzkf910W|44gRqv@07DGz*E{}kE!{XZlZ9e0hq;r!R?Yfq4uz5UfP&gDB>E7!^A z|K@i2@ehSx>nAD_BrHfShM|Jb zkX`J#ZriEdh0JtfO#utA)PrCSEUn{Hm_gkI)ZF^B$z)8CoJs0kIv=t}1wFmXSzVW# zJnWvt%h+2AbVHuaVoDaJmVP5Y3YJZ@feSiZpkITOWyrxNx(n@F zRq6-u3tBAWp}g5qV1d^}0$Dt6P&*)qj%q_ zR7yH*5VF?q_(~x+& zo_z+u3!o21hhxGR7$sK7`4Pg2oHEpUVV{NaB7`20xqBQw22f=jv_s>v-|OrB6&@R< zMnAOg;;>lv?2_G^FOa7{sVB>GB1>m!zv_%fDGUf=03_X$%6jxDdEygY*|lqz{kJmJ zr>+MfQg2?uO~=TI)r#D_cfafkPLM|}LD+*I?v<6bj^3BH^auLe#`|Csy%X0eGW3>^i|M-?plB16eWU;tMw%_{D*Lfg2Sx$ItPc|JTxKKIu zv?V#Qe~;Y$6?ypU0r%u-PYoV)Pa5EA)_1-DS}%Oz3+2^FOfkgChon8nBTXXf+Bh z@d?n5?In+{L>S2erfjSyl*1FB0$BP*a2E17P?i=YH&_%6k^CEIzop-Dnd%#PB}E$4 zP(Y`TDQFli!cqxR@4Vu0SVnbRz|9^~NoFKwrCBL?W_C-$=KEwmsD+~GM>qO3C$d%w z4nXa)PD^Iua$>{6y~sCZN>vhIFt-r;KE)*mkCm18`!AWFB9b=se*k?}V zz)D+|2Ch1EVbDk+b_RCL}AK2fP?y;?Jz?{A!+wcBydG_OeRQ_{o zPCod_&&d@x?*2w6FXgE}Dlh+)=gWqFv<$Kv<<2jDTE42tw(tiH zeSthrU*WIcB)44gX}JOIgRVTh^?-Zw%eW_Ju07zM+#tJF8*O2;zP%M{rRIrGd?G>X zv!DGefot#Hz27BCJoej22}NvHfJ!7BdR3sVlymRnATn(ATzJ}6)CFL!7AYAn#)Mqk z|FzIY&sUrkd}=*}O6u#l#{vW3Fa(2MxHIL9-n9pM8v$lD&o2}}@Z=8$1#1nI(;UWq zc+%?*w4ArjO|;dHsJ|a80Hh2H3#6wf)`6pXQ4%e?ePu`bBn>r4q-8Xawiv3#s`jPQ zlkc@G?Xn%1TRR$hp4QUJ0xSkXiHxshGJ^_yt-k7W?Y#xC_$0gl1O3bQ- z1^ZG_6c88j^ng-dRawcDf{p~Yu#owIvdaW>S{}g^5`U;CQ3%F+UB)Q86k*`nkk5q} z0L(Bb5haKpJ|?^O$`{`DM)~8%ENjzM`-Ho9%j}=E+&0`V`?bqDORkmAzi%qeeaC-o zZ$AL-+z$E3TmM|T>vzj8)aK$nvg4}v${$zv$nkBxSJVCS;j2C=liSDo9ZqD&r`{nS z-g~kfy{Levja)ePuPVr|?bRl+TrKZ>-5we5#`nEju73BMWq9(@vKVidoh#oA2&Qu1 z^`DiSQ3db<8ST17?%jU79FUGoZvBM3`+wdb)7@XY&Kp1cHu>{=PLSh{=qp#B$;w@~ z%U!o`m)#hG9$l@kzvBU~|B7DUP_J*F+^0{C>sNe0_T5;>o(J3$(RBW_|k$ zb6ksyi*n_aSIWmf{&9lV!MOYH#)2=b7rC@{!n_vUG8}uzXWIz-Y-q~!(X?bLMwJ-w zz&sZbQ22jS-SZMYyt~|6rzymK5CNu4k{8FqBP)&znQataOUtODq_p07=LMY#R!i6> z;oyLiLxvRC6cmAQ;>G5TD383zSUyWqGrbLI25V00h(;LYu4eu09?l& zd#pa!jbwFo_1k7z4-S$NXWPb<@YnmV? zM4Nc9JPIn77<0X#&~=%(T!4|lJCH6I2|=S3)XwaMxx%9a2h|>oL};w;W}L!ic{4P7 zCs0-}!pbw6+V47y8L2YQK&7hc;6^JydNrFvEfy*m)KL3OvLeDiIr0F}Rz;lOB z0cq+ITrjnbK|dHfp%$AK_LW7RInP5FGEHl5NJg(9HpMltam*E@68oazaDpBfzM3@4 z=wXQ8!AZn@#5-p+t69zZZZ4Ri-*wl+mAC%u7K=mxDLmLCK$zfy3Dtr^H^%zvK1nm( z5}*Vp3}J>Aoq$kTQvku9XNe+0qXD+2BA?>8v~=r$-GY%**#Bvl2l5doy4Sj_OwHD3 z^o?cdswxcaAb0?~7-|!_#d)PvT5tiTtAxuko*61y#0eZV3O$KZn{~rUCY$El3;=6` zGQy(@6w9UaB3}>3bz0QV6d(ejI|d~5v7#UBKLBiS#=yQEwgR#cZ0cZOVaH=w8B~z8 zZvvwO$dwiQa(Gj>6AFAMz-9na1;rP^{XM0FIa~POg9FECF4`{^TNv~+Ald}`K0ROa zg6$!UQi0ilFyZt3ILTP{3tW>Q;=@7fz^1?}s$NS<1)?yrO&P?DVhI{yI7~-2nYZ?K z`aUtZIc$JR(|3`-ZYnA%hUVPpE=~o7sdo++S>fTjJ}ZCrUp_0%YF4uz-okzg7mQ96(oDR_Z7+t= z69j%}=Rkcuh2N{|&<7TKGfIbO>7XnK4ugsq5;O2Jn4d59h4z9zkMHxrzwmQD)MJ0o zQ-`W?*gs)h3;B=cF6BD+J6U8ECYN)Ty`bbkpd=I?7zmCPT(mvM@*L$dm<7Y- z!|DWc4469$qS|N|B|)bng?=%5I#xoh^SJQyW;Lr>%{mkoI7_eZn7!Rlt8WT8@Kp7} zk`*~%NU9a1O!v874-eykiA9z$0~`>jKy(y_MN^W0`**H&gU91pyXOva_X&T;FM z4COewht6RD*|9h%L}vNz@StFzWa4iDOuYkhCT$yT9ox>twr677Ht*OrC-%fnCbn(c z_QW>tSYMuZ?_Kr%fv&EuzPiuuYaQ#baxFXwHF^0#URKp&wb6S3=uK^zTnXq$0fa3rvhiieCwBk3p5E zse}CByq5m=iq4EScvWN0liV>iMVRB#)sSG7=LBUih zNG~QCXa;Zi*v>8ooUAwtSm=T&0T~C}MW&91lof{-BJ*_Ai4tB^7nOkMe`ic5h^le; zL&|;Ag913Cun83gDdxvLb#{)FLP-+|>|D@6&5z)il1-gnGkkUQOi-#DvX~&)M{izR zZ1a2n?~;lHxiV!&(`$8?vho)WEajKqBPnqW;4_T|>GTzn{QULn@qAJ|au%*n5@nrI zO%XNv<{0&jl2L7$7!y)QBbyH>LtB(UQ*2{_$w+{`yzfLC1%QH)YoZFe8G_BQUk6NZ zSHG)bDg2t+NolbeKxNa`@BA7*4~e#lap z_A>0{RJHmCN9t#$pOKS$egr2d?O7t+66r?-haI+WHcqc0WEU=%-z;nHCXoIzV_Kd{}{S~52Qf_2O zSJQ<64u&dT%Ss;Et0yAW+h5$hCq}wI2LQV;tz$Icm6NSVK>(tY7Q(A zV-7J8Khvhdgr4nH_QYALgSZ?_kane#YV~T~ETh1t@E^PjIMTxbn|OwYl8lZfdj~{Z zAA%Z06qPSuFF1Fk{6v8VI22s6{!Z?G-EJ5z2x}vpZ-Z!H%3}^xyxlX6OoAeAKL)m| z$_B`?Tp1M5gxwXWOH48Y921N!0|bYG*5cLX`Zs7{dOJ95`{PN<rL>e_v**m1uYm`y6gd?eeW9Gm zB$2)p?L~Z(G}{#?cLCmYoX7RRbgEPLUlJw%qb9l&A$89hY>Gjig8ciZ@vy zC{s`1F*t}v|LT9+Pp;|n=Ek}RKymfI({By5(ev3!JblQwP~o+kyLF&+on}WGd$9N5 z)d?Iq`dR1OQlyqWfr_Y6e~E9y$#H~Z2{Xh#VCd0`sY8V2`HW)7tkQsTLeO@Ei}bKi z>m0JWTglHk>rVtzdqcLL4|p1*aH*ah#z^%aCygPzW`l(c{4i6H337=K`W+=G)bCpwDnW)~cQ<^)Vi5HaFD56lmj zXo|~Vo)$;O*_|Zr(SVF=I;FI7e(4l@89Df%&>|)xS^W(URUOCq9wf<7gruNzgjSh_ zCXgfCv8XE+kk4oi2ORI`UlnVGAj;ohvw z%hTlfSg^;nVilwcrIhPe*Re%w>5%`iDt|)mh(N%R&;O2=*rIF8l0e(YWWBK;V#)V>JLU`k>w0M3p~)|%YKu7h`+(Amglof)p=BaOoq zQ!y4L$SX8m#-mrh^y%U(a7p8&4SHy((P45qK8{+xQvBVH5GTTejKm0Eku)g>GCZtd z>|p0j!E78`39yKTGQt9KI$2^yrMUi^pVeX!R7*i+pOXxLc#6hniF`Km?Or=nk2N=` zsQvUmxVe)sxE6{$78p}DO8<-bn5i{^D-N-vCI|rP;a_+(am>u6{C7BDG@klRBT)G0 zpX*5DS`fj*=KUwKaBQ$vY%$l547Vfud7?D}fUBd)H#30wOzdJ#QROh?9AVx|s%ZIX zTWYL%aSy4s;>ye+{xCP1cz0Vr_(-(Jh=j-#F-D5hwm}?PbY|4p{_r3-Nu?nzgYpMX zXUHMqzf8p6D$wLS`vk4!WQh*yk-`_?R;sFPEnVPeJuQ=hjU%V zRA^ewJCi+TZx z765n%7cByudE6NKH)dH$-F&=LT^obzrj6k-xfj{kFh8@@^|$X&t&NjjgN1ajC=SLc zDmkiozZ5fq_{y@%$=^P`G%&=`H9kB*Ci74mlw3_p3D1%9_io2JA9&C(h&;%Y?1Z<6 zz^kzz-vXo%r3J13BC-wp#Dbq%iOh%}nDyxWy@dvtFZ34NBc_E^<6S(P*`eA&RQhf; z^Cb=lqKli-zmV;M&ms$Vgz$yR1o6zd|1x)qTE$RmiulT}ss#JvMskZN2A%M5$c zrR&3z$fA~2De0;^A1FXa^C~&q-rixAF!JI%*?s>ozf9{US*95lx6p9a33E;( z+dfeCqD#6@?nOlJa;iCv=L>iGR7_4aW&JA$>eM0zTCttbY)QOK9xojR!=9*aOu=Pc z%|HmqGa#6j0p3CeY&yN_PcH~_S}4O>csYR|{``ke+36crw+p+iF8%O<%S5oF;ySyD znzJEb-(!t;7C6XGUS+xlrWguz!(jNHSf8IZ5|tY@(aW(KpSwRkLspW48x#mskZ7lj z1Ssq{PDB#JpWBe#KNNq62fXwv0ld**HO6MTCx0;uEs(?*V~iO|L8M}TO(qz~m#(DI zy~$c!dE+d>#q#K6Yjq4neV2OcTqo(+Z<1_VYI16PVQ?K^6qvf*H#x0*Mj#*$jg|w5 zH|40t(Pu0s0<5c3GM5iKTDoOMu5z4WY&NsTOrE-&{yl!&{W>@EF_#!yoAFMHu6YmM zU=e)~`<{4ebXFU79+7Tw>FjdJk&3(%D9l3P6ZRWE3J!%J3s8c_0wCcAd|`Ng43MjY z?SzJTmyV+9LH_c(5{3svI+sx5djwKN!Ix?p*E(pe^|Ilao#^G!gx<3-2^#QH4th3#>JpIJI;4*yL>}Cs)P~TzAUrhTFo6 zA9qL_*XadhdL8Fc-vUv0Hyeu+s~)}!494Gp0GRztjcYuBhV6&kEa+x$afssMrBrK6 z$}89o@+Z=Kc7F32csiH`W!#3yYJ4xCIR&HNwJUhKJGitL|6e>BK3(Qnv)n2(_^-fN zfSR%}GPszBRGtqkAYcBCVQT3&2dSgiU#g71Z#N85 z6t1-6!82!)sqkA!K$QcpldgR(pP^qaH9mvz=jqm%ylYaQzI+{h%G9$Uu2s+FdF#5Q#XA&#Ws)5^ZW5|kP;bw;&2XiUQukOdj* z1TrL!qw~2ZkFrZuw%HF(rQZ1^-Zb-neAf|%9ZiMXbE`88F}ZIkU!m?B%tj#GfM83IX9bC>(-oG92Cqg^e(9)s9c|tdz-o{)!7osy`E) zv4%J#1>r2ic>wz-0>wCh%T-?3f-`ot$z}U?OCIQr8I6R_^&T-YHpkO4aRn9-)BdPW zvvQqmMj(9tj4hzi?!HMLI z9_znDXf3Y{o_8FOeeL^rejIv)g@{mcL?23(WD}QUdlgpxM^k-ueCloiZ}qqSm%Q`fwV#E&-omTCUjaBF++zjTTq^nRbK z8JAL6rH$n~R~@?+aHEaV@zd0Qnl>Np>Un1MTwrh`lzDr-dq#JgpxSm3l2xg zj(ZM&bsrVRjuZJ^xXN@nxMp z@0uhWj>oER&Tt|UxnZVthM(XskY^_1RboG+{hI_H*hkpOor?w_ZvXIOQDWdPR)`M*j9UE@;-6 z0o81s>bo4u8?HeJJ`m9~a9wr4EcU5reR4qb7qv|X`pJ+v917?rbFmiz2!7RJycJw> z7)3~GT*C~FKM=}GcjdSe$4h^dVBp1z_c~e@)mc5xaBo%it327PnMaApq=lh(~<3bLZz{{TU*4ukk{lmq*>AAI--<4Wy{AZM( zcy^+%s^3S^Ok9;t%lPkCCi`S}LQc7u*KRh`1%Y?4FNCkIJCRi!qGv7hF-Rt_2wv0E z`@80xewG|E?9bt3r(x?YO0QKf0y1oyHmR-K9(_uk-)Bz;o7R~Jw+i0HyblIhEnvNw zGl@D(-r1cy_zLMR!Nw+Z9UY^?mTl8zr`ji6PHJMlTQJs(d9AgZDtDJm~&N@z; z20ilG&Yv~oI@g*hBf8H8cYFIpXWjT@5|^(soUS73k zT2=LdC7{>eYGAlFVD-v4{0vX#%KP2~A&mC6#oDjWSGw#khU#0EyZV(ZI)j&ynbFs2 z@a?^{k%^*6rr*!`_lFy)U+F&0-tD}XNfHwUFMRs+0$+)l2Sk2CGar>D#ty)k4ywR?mS*;)6MQ06uQlG@!dNKB=Pxs}=cV0OkDu@;YNbt6p)NM}}8A^LG zpI8|F4$(Q8?v^ox!o9_Kb~VPktrjDInL!Vo#_+J~t+$PZPqsR@psmg*AinL*R9u+;idG;N(YqF0vhhfu`gkH9pKU-8prRD~ zq`v+b;eCl>Dn6*EUS@jW8H5ta_+OL!qM<<9akIMO$o1qfxt_h-yg|Ao{j_a>*lA&+ z^$V-JX)cA-qR?(NOHJl$J6MACbY~Xky)JlNQ&SoeE@z3uv^nrY)!U_Z1{uhvJ>H0t z1Pct)Ml~IXNnurqA(G z%t`r7i?U`JWKI6e6*)WzUFLJVrgdIr9V^wOHrHBLbjohU2!;?6LKZ_+7311lkBxX50`e@}T|zoZg1cuGw`lWFxhmuxi4{m!|Jo6``W zCh9o-^Ico=>YW>P5wG9}Y;d~P>kw1#q2;;em2=Cf%;^yO4HOi18h5Ox;1nlxUNabj zbg*;$H_zwLqu&D8VSLqyTieT#i~_qJU);y3bk5M|IN68d%(S7XB? zmm+VFA}4Vcd*yvIb`_6T^N-P(p=BbT^V<0ER>3bN>pq4k#h_24;&)5TbL7=(PN#-j z_I<@&sLbQUb>|!RTKCs(snf*|4P+Z?y^rA)kMRjVz3J}#>w~ABcG@?n?XxtVL%yCj z_BKyf>IS9_YWL5Q#Dm}|+jJhWr=Y1<`-k2tr~8N2<5#`SJcPTmoTLQ~&*IvT6zdY* zhf|(Ob=wtvErGL_yL~2Avur<6ewWS2pXUym-xa|!(%wasC=r2CA z=20W3^9?9H5{0nGR&;EuVlCk)iJ310fa(FKeG6{WQ&eM#m;oV{%}m#trSUDB!qq~| z)+?VO-{r7(yfg0Bzs6*L0DlXKV4mO#F;eBN>@FD4e;&Aku@qg!C7x4uwx=Yra7EDE z&FYF6v8smBWRaIAIJty#2%8udjko59`yT{7gws43J)@g#Y|+HeSngCTx`=5_W`M;8 zog9V#;m*Ug2VleAz{)h(j91k_F0yB$b!g=obRbeChoqg}vxOEbT@4O$9FO^=^0DG0 zyQ)&-HsH*ChbSh~u#Ah~%XLA9@qHNe;J6*IK}FfB=8%qVLZ%x{&Q=9~_98r`gQm0p zLBfSfGBqQ`*-jm>FlF7S;5mAIJ21}qN6y46l2fZ7dB|^13T^k6=JWit z#bm(yJpa&PvNFvyk<+;g$90-@Jieg+NOnio!B3gfb*FN7s?e>a@4mz0G-&)=Z0qx} zd*h~A=1^}%V8>Y3TX=xpz)?{z&2E(Xk;tLPi50-9vOjUW4IA!E*x3gq8QUT9d5Zj{ z&fnv_RQPtAak0@RW7bvm>aI;^BEfX>Qa!pNnM25v%>vm$p9;Ko(Rh93izT}27_YMG zgQA{(^S|gmwR>6A5uhbkZTs~7aXrWPF>!aMp-ZDP{Z8^S)APcXu<^`s7faM5-0e`~ zlc;83{Q9RFwa25r^O+)6efk0YE z_hSl`DenvBHZIa5TTrH}A&5h`5B`bQWA(R$^Tf`;x@U;b&esO50r!X0U18L3ozEv{ z`=ko~XLZkQcFX0wPJH-;EK4T14~C1Nzs(X=;73gsNJ@npI!XZ|?+Xr#2Qp^9%{FkPk9;ObT9{#w zmpuqtv`xv58}#7xs+2BbIrA84%X?}3vn4XI)n3e`q{%t7GHLK)LgVA7#!l>zXXK!! z2n3}BmG%`_6-#3{oiD@vE`ZDEOQg2okDneuqw&DXvZ_PH4G0J}^>$pr_kce{>tBJ{ zA*~UNkSLpWI_eT!=CzdLF(4NDe2WMv!3%qXZm@Zf(|og^i4Oi%MU(ImS!wRzY~XiO zQ=VBQAY_%}l3y(5`2FDF1%2TdhqV;+m+4B(7g_X~X0E@djr)5h?zQ18`nPb8W=@3B zNcIZ%CUF;8;xdJ2QC(+~cx5ZurnwEKx9=U>oMNY5xP)|i6c})kM_BJ&_w5))m7n
    Y}Bz51e`F7Z<5P4^1 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ðR|%AfxE5$QeZwVssQ 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 bsqOy + + + + + + + + + + + + + + + + + + diff --git a/client/ui/frontend/src/assets/logos/netbird.svg b/client/ui/frontend/src/assets/logos/netbird.svg new file mode 100644 index 000000000..6254931c6 --- /dev/null +++ b/client/ui/frontend/src/assets/logos/netbird.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/ui/frontend/src/components/Badge.tsx b/client/ui/frontend/src/components/Badge.tsx new file mode 100644 index 000000000..c5e2b5f22 --- /dev/null +++ b/client/ui/frontend/src/components/Badge.tsx @@ -0,0 +1,43 @@ +import { forwardRef, type ComponentType, type HTMLAttributes } from "react"; +import type { LucideProps } from "lucide-react"; +import { cn } from "@/lib/cn"; + +export type BadgeVariant = "info" | "neutral" | "brand" | "success" | "warning" | "danger"; + +type Props = HTMLAttributes & { + variant?: BadgeVariant; + icon?: ComponentType; + iconSize?: number; +}; + +const VARIANT_CLASSES: Record = { + info: "bg-sky-900 border border-sky-700 text-sky-200", + neutral: "bg-nb-gray-900 border border-nb-gray-850 text-nb-gray-200", + brand: "bg-netbird/15 border border-netbird/30 text-netbird", + success: "bg-green-900 border border-green-700 text-green-200", + warning: "bg-yellow-900 border border-yellow-700 text-yellow-200", + danger: "bg-red-900 border border-red-700 text-red-200", +}; + +export const Badge = forwardRef(function Badge( + { variant = "info", icon: Icon, iconSize = 10, className, children, ...rest }, + ref, +) { + return ( + + {Icon && } + {children} + + ); +}); + +export default Badge; diff --git a/client/ui/frontend/src/components/CopyToClipboard.tsx b/client/ui/frontend/src/components/CopyToClipboard.tsx new file mode 100644 index 000000000..1b2a87da4 --- /dev/null +++ b/client/ui/frontend/src/components/CopyToClipboard.tsx @@ -0,0 +1,126 @@ +import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { Check, Copy } from "lucide-react"; +import { cn } from "@/lib/cn"; + +const VARIANT_HOVER = { + default: "group-hover/copy:[&_*]:text-nb-gray-300", + bright: "group-hover/copy:[&_*]:text-nb-gray-200", +} as const; + +type CopyToClipboardVariant = keyof typeof VARIANT_HOVER; + +type CopyToClipboardProps = { + children: ReactNode; + message?: string; + size?: number; + iconAlignment?: "left" | "right"; + className?: string; + iconClassName?: string; + alwaysShowIcon?: boolean; + variant?: CopyToClipboardVariant; + "aria-label"?: string; + tabIndex?: number; + onKeyDown?: (e: KeyboardEvent) => void; +}; + +export const CopyToClipboard = ({ + children, + message, + size = 10, + iconAlignment = "right", + className, + iconClassName, + alwaysShowIcon = false, + variant = "default", + "aria-label": ariaLabel, + tabIndex = 0, + onKeyDown, +}: CopyToClipboardProps) => { + const { t } = useTranslation(); + const wrapperRef = useRef(null); + const [copied, setCopied] = useState(false); + const copyTimer = useRef | null>(null); + useEffect( + () => () => { + if (copyTimer.current) clearTimeout(copyTimer.current); + }, + [], + ); + + const handleClick = async (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + const text = message ?? wrapperRef.current?.innerText ?? ""; + if (!text) return; + try { + await navigator.clipboard.writeText(text); + setCopied(true); + if (copyTimer.current) clearTimeout(copyTimer.current); + copyTimer.current = setTimeout(() => setCopied(false), 500); + } catch (e) { + console.warn("copy to clipboard failed", e); + } + }; + + const resolvedLabel = + ariaLabel ?? (message ? `${t("common.copy")} ${message}` : t("common.copy")); + + return ( + + ); +}; diff --git a/client/ui/frontend/src/components/DropdownMenu.tsx b/client/ui/frontend/src/components/DropdownMenu.tsx new file mode 100644 index 000000000..d43c37e1b --- /dev/null +++ b/client/ui/frontend/src/components/DropdownMenu.tsx @@ -0,0 +1,233 @@ +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { cva } from "class-variance-authority"; +import { Check, ChevronRight, Circle } from "lucide-react"; +import * as React from "react"; +import { cn } from "@/lib/cn"; + +const DropdownMenu = DropdownMenuPrimitive.Root; +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; +const DropdownMenuGroup = DropdownMenuPrimitive.Group; +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; +const DropdownMenuSub = DropdownMenuPrimitive.Sub; +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const menuItemVariants = cva("", { + variants: { + variant: { + default: + "text-nb-gray-200 hover:bg-nb-gray-900 hover:text-nb-gray-50 focus-visible:bg-nb-gray-900 focus-visible:text-nb-gray-50 data-[state=open]:bg-nb-gray-900 data-[state=open]:text-nb-gray-50", + danger: "text-red-500 hover:bg-red-900/20 hover:text-red-500 focus-visible:bg-red-900/20 focus-visible:text-red-500", + }, + }, + defaultVariants: { variant: "default" }, +}); + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + variant?: "default" | "danger"; + } +>(({ className, inset, children, variant, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + variant?: "default" | "danger"; + href?: string; + target?: string; + rel?: string; + } +>(({ className, inset, variant, onClick, href, target, rel, children, ...props }, ref) => ( + { + if (href) return; + e.preventDefault(); + e.stopPropagation(); + onClick?.(e); + }} + {...props} + > + {href ? ( + + {children} + + ) : ( + children + )} + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => ( + +); +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; diff --git a/client/ui/frontend/src/components/LanguagePicker.tsx b/client/ui/frontend/src/components/LanguagePicker.tsx new file mode 100644 index 000000000..7a30f8b33 --- /dev/null +++ b/client/ui/frontend/src/components/LanguagePicker.tsx @@ -0,0 +1,235 @@ +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import * as Popover from "@radix-ui/react-popover"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { Command } from "cmdk"; +import { CheckIcon, ChevronDown, LanguagesIcon, Search } from "lucide-react"; +import { Preferences } from "@bindings/services"; +import { type LanguageCode, type Language } from "@bindings/i18n/models.js"; +import { HelpText } from "@/components/typography/HelpText"; +import { Label } from "@/components/typography/Label"; +import { useFocusVisible } from "@/hooks/useFocusVisible"; +import { loadLanguages } from "@/lib/i18n"; +import { cn } from "@/lib/cn"; +import { errorDialog, formatErrorMessage } from "@/lib/errors"; + +// No flag icons: flags represent countries, not languages. https://www.flagsarenotlanguages.com/blog/ + +const labelFor = (lang: Language): string => + lang.englishName && lang.englishName !== lang.displayName + ? `${lang.displayName} (${lang.englishName})` + : lang.displayName; + +export function LanguagePicker() { + const { t, i18n } = useTranslation(); + const [languages, setLanguages] = useState([]); + const [open, setOpen] = useState(false); + const [busy, setBusy] = useState(false); + const isFocusVisible = useFocusVisible(); + + useEffect(() => { + let cancelled = false; + loadLanguages() + .then((list) => { + if (!cancelled) setLanguages(list); + }) + .catch((err: unknown) => console.error("load languages failed", err)); + return () => { + cancelled = true; + }; + }, []); + + const sorted = useMemo( + () => [...languages].sort((a, b) => a.displayName.localeCompare(b.displayName)), + [languages], + ); + + const current = useMemo( + () => + languages.find((l) => l.code === i18n.language) ?? + languages.find((l) => l.code === "en"), + [languages, i18n.language], + ); + + const handleTriggerKeyDown = (e: React.KeyboardEvent) => { + if (open) return; + if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + setOpen(true); + } + }; + + const select = async (code: string) => { + setOpen(false); + if (busy || code === i18n.language) return; + setBusy(true); + try { + await Preferences.SetLanguage(code as LanguageCode); + } catch (e) { + await errorDialog({ + Title: t("settings.error.saveTitle"), + Message: formatErrorMessage(e), + }); + } finally { + setBusy(false); + } + }; + + return ( +

    p_r)^;F^SB&oAwAUvat>AaH*WM-R9KsutQD*d z*ppvezrwnFeZSLMV|B`R5V7+}${C5y#4e`TftKQsTy`p*68{mEm2e}0I*&@t%VAM0 z-`TlhMdy)yk%i4wY|L3~G8H?ceDyAAvHaq&caL{ix?Z+=mz%NOx~G`*r5!k+ani_E z6gcNt;ILzu6h~kPyYB{{vwLKu`^XJ%ISppB!AZd5Wa*1T9eX;4*pz%*Pg%(xgPG`5 zso(%UybN?I&h}89M&zWxW`qtvqzS~T?m(YgJ96gDH@|#tr8HEJWZxK&??#r)f)yk| zM}6G4<@MMcit`9F02T~SIS$Ba5~Sc4L!ZB;p|SPPe@1>=i{Cckp?tSBt)2VXUCk6KFr89CN*%x)S4OSQeCDHf}K52=}Tof3sqYG&a&s(wX!JVMF>wiSA#`X&w- zdnjQf@*cI?l&xEtdrxxj@{VDDxw*Q*Djn|c|14!-VnBY3l{Yms)ymHkKAn(z4ZtVQ zJEBM)ScWr=VtHVCWXGyGg*XUvv}eWG;NaMbp1#r1zS4?{QoQ2U4&Nz1fMxvEH6v3~ zBk~o)Yr7ll>{5F}gI&JD-asSO0dSHq!tO}Js?nl>T|k4Rh>=XfFk+{JEbK{0)w+zy%i(j40 z;mIf=xuY5j?6gmg2~;FI^7`u#jA2#D(8EgfKvINPx5JD?59oy!1i87gl~=b@ZhlIw ziwCL!Cy*2db=*UQp?*hCsme}A`04KbZ0z;W1Kz*v>G|ckhrFpL4`9mYFr|rEq1r^a zZg|djwW=KmvU4=VIJ%*VkI?#QBo!43r8)gry%D(hXw8uPH+HN&ku;yLy^V2`Z76`U8jy~f*L2y6F)_+;KL>>%}eZiR5(T@$<>Yd|p^nhDW; zA)gD0D~<|~CNY(aM%O=^oB!!*Y;?tKpJZLsF8|OoZtZV# zUwI`CSFPOXZf$jg0=V|(FufHvb{tSi6%(VNbFl)-d2O;~x&CDz9X>pG)v?*19=mGj zn!~sEs%N*^0X7A;#Y^FJkGPf!HL^5`lD=hQwjnM(#MU~MP*$fK%*(NcSzISdY)eRz z2CObAmScoltDvh0`9(xi&H_fzRSK*_Mpl(x#M6{@v5HfFyaaKkv_gJ;Uftmqu219_ zqfDQ@%8Ec0w|=25lCB!Q#I@_E^UznffI*h;i)<7Q1k`XZa(cqfq8fVM;<+t8iX}4G2`ZcPB z^)<=R?XGB!jEwAcb`14fOh!w$saS8q>K(!esTd*P`5j`1<>w^>d}&~T|4I=o2{Z@} z+a!G%e}%u8AYC8s`PB~7jnhBjUicDFR0D@R;Q2X1JwI)tw8V^JXDpI!>G8>z**UQStH zw9>(J#h||joyQ{zivJx*o?~(TjsE&-yUppe?YX$pfek&2UGhuohbx^cDrseEM%~)s zF1@+jSX$X|5`hr+aeRfhs35#Zz_AM=WL!GOc z4Y0lV?()rGYPIsEL8*|&D_`1eFeOCk%k+$l&Rv_g>>WW-mx~9sMxk(}0UNCev!{kT z_DwI>%YS5P1$~qIdWNYl6+`5aP@XW~S<+{W`v2GO|6iKQ?5U4F;*>%;=R@m3XJ(|@@un}svt%}9YqUF`(Eaxns})dJq5K{l{c^b&nL(f6Q$C!}w($%OZ~%e!k? zE4;_RZL;B20n=BM-HveY@f#qCZ@_yr$QJhE?5X_#hOBqs-GI8pu#PzWaJd>3sMG%2 zR&UxodfRO_hr@Q8WO#b@GT+!hWqEle?gmG2*``zsglmyOMuF<;ksQAJ$h$kJgB?4l zgB>seLwZoXMKM13bRh_Yujp90VZ%!F0CD0VU(xXNn9n!XNjvf~r&*d!)Z7`V1+$@e zM1U(fNCXoMHrn0{briGnA$C(-~!!8?J`_yo|xqX0FQ%^lL>@|6ZS(^Nh z9!s&QnDmR184g-(Cde}0)6Xh5@b&cL1 z{?OZ4=fRhqYHfyfL)CR{u6$oTj0*@DN=|CNw!yo3^gaL1%P+X@y6VQp zmlIbu`9^0?{r0yk|HsvqvTCdpyhQZFE1hvo6Yyj~&>{Fxb3s7qpOv`U`6;=vG5&j{J6eNLHpMgZ#rM#xGqIr_sd5 z?;U-jr>AFn29ocFB1%v#Vq0cUp_YUc(FIsYB5Qa)62>>qm;BH84ym%AZ2o!jpW^Uw zxF0$rO~?Sg%|gE6qc@C9ag`w%9$UGTl!p91uAEm%f(62)p$;N3pGMVmp_L@qiuT5k zN)qyEsmeatetFxJ9f$bUQWEkbqLc*i2DqdyvJWNf06^cW*%sWgrKX$u6rpnZCay|ba zoDbV-oG2T&!9g?{-EE)fo7>u&F%zzxIb^U8R0z<%a2A_*5=bBLeLlbOKQ|p3Ja}UE zrzZ{$9=hr7UY5fa$^Vw$p&Ve!{UtL9Yennp;Jv^H3#kr^BL za!17C$tygkmd`kk@2Y;``W3c@HdOR!t+%gOuLyRs>V)M)(HfFY^LEkj{c>OpaVKcA z-vC1O955XduE$@y_swAjfuv&lMxFqNi#RK%0aOgqF^qpI+_B0JT*QIH689nd4$NnH+|M&l87x|c;Wg(XM z5BW8|TEZ`&9$#9DHv8;9;9H*U}Oz4Uf3UzQt z5c^7VPQP_1zpkXu;K(1US>?PB`$lYye6qBmfc-_zFK8&GZ`eP&Pgw&a;ufXUr%`juoRg zks1~|1`MmwaUenmie4QUgR<(vMsWlqa>@|3YFI8XfEouBdq}XasRCZ8y(i+AsL;hN zvV{tCty?>YbEaVhx)7~W3v^9W`iNSes|_lvO~p(ORwS2rJ}W?B6sh7*;o`zrJ-SQ& zk)51bVP%5EN8!#kt+KhT<*f$$n&#I2%q-2phR)7Lhmq2&ZJwSP>B;7Cd-*bZ-AIBa zx^~d&nR0YBFTUjD+5YC{{$5J8>1>prZ0;vMQmrJ&ovd*CXfD(^g2O!DGGa=kiCdjP z4+NQkl$Rr1a9f4ONWy?mj90s=UeVB-KjQGV*VnhEn!7glu&d?QC$8?=6=$1hnZOoE zPmgoL-LY)Uy0o=@ux_BIb7WD}J&{ee@Ar%8)a$!GsmU9C9I9 zpS^i?O^dUpxoWwya%4RocT{VstUxZ##e2wIv{%>rH)>7YHO^kd{hBK(+`6TnyrNQl zvAM6Z)L3rTcMSuA>A4P~zlcK6nvoLilq0zg2eRIIcr!gyIghJlZ+k1^imq5UIl1Ma z{4o3e!~TI*?`4%W35h>8cO@t0#2fM^yIQXu-9F&|rtiW^Q+;|#g42*znhrWA3rogG z(Yzvss;MZaA!tDb=tk94^TX@mnkryH(N>@vmH0?iJqA_fYjN-2?`~mBFLt-IxQQZ2 zGL!8MiqLVv3I$9j*P#ARpl-<%EYDDQgesQQ56BlQ)mb*+p3n*}sy3rMg0OllHDR?_ z<`rR~(ZSP@ZwAV>!I}2piMe3fEwKj9VLMRaV?}zZt*x~Z3`o-APDZhnLv zxKvKO0hf?q=zkb%SOD&;#24;fsl-AHr`2Lv16A`wAFM^x#o8ThZH{BtwG8gOzB}Js z>Mcu*%RM}{O7AYkW3?t>t=3BZNVjyXrKY;x=dJ70Te8-*wHUpNi(@VMW9^o%YON+= zS-pL*vb@h)O3&}(HgibCEgR7ZJok}%8ngpNPJ#%U@dc0;_pbWR@*yc@s#L!?YE3_O zlMy+BR(-D2GDNb4s-%E&gwGn3Es(omQbmy_VA3&EnD>meKO4&-n)2{A;(C|Zb*(HR+ zV}zTMEL6G+6o>puNkgK6@J922OyK?b-$K5RD-lX~# ze5m($>hZ#}&6wW<}#M#4~5kbT!;s z+eTVh@L4cEsrUgDbrTQ>7UD)x&?ahHRXyZt>T7ZhRr_5tGu5kH*a*9;sg37$R*`>)FCqsQa64gaKnwApUw z&z|LyhmlH z+68@H(T+Zpt3@Bev!y|rB!`$FU=xP!+ps>xEc__2ffKOWV27N$lcVenAofGKjZ#Ds=(Y>|oEu>a zP_9*r$YqG+AWBvCAVcU_uUIet)6h29(eh=f;ma)@gRPC#t{AN=KTp@K%gZY)%=;zY z;zoLY+O{-1c3>bjduiKGdq(POYU+C}rYD{-SuCdaOUuei-#76c4kbbmI46_?yHzKd zX>TXW4mX#Zc$rzj^VAtE_HaybPJVvFvgE$_SdAt=e&^_;*s1u|;_QK1yZl*bhzimR zTB8m5w7Hz#$S4txbDA-vYv?&Cx51avsg2iY7RBtJcv_!YQo``Kl5 zCRzTr6uGK=&~BGwF&fcy1yE{+-9t~229a~`9M6$PI8Tg({11J_efQjVbIpr2jgPld zN&VzSe{|j4M3(hSFU_)2hc_Xj5`lpnM3>7*j&b=20ixxnFKKtd zkpl3s2clX}!ERKs? zoUq}sf}$cEv0FRRx*{=Qv}4s~QzWxS%1*PPJ+iju@#8|DgF(zjA_NWt3o)VfKjp5{r>PWo7bKCsNc*@}EueV$~voZ{vi0Mv&MkA?iv4L6*2I&p)^G`ma1*b**z=s7mV zq&k`{wd@Wxq#eBZQ20xTD={zN$5WBevGa@-eQ5t^k zk@^xeu=XgckuM-L%=YnV;OXGXmFbuTqzbOhq7-y;I@D(`ra6oplQrM*_1|+;lGs%> zhmrMhd*O07b5du2{xdN8BkQ`6CGr*Q(j_D7tcW&Uu<|2Hl|}dT#8@R4SF91`%F!xm zAOKQIP{VQj($gBreBeLmJGoVUy9Mz5s?fKDeHZ!74Z8kZdB7;)6kKG0-V$K<^1Wfw zx`3578o^E~iDhw#QUnI(ivW&dmJ2Sq>Lj-9bR|h& zP9knLqJh;`bY{``YV<1(K2s)-NTuk+9;YBUurilrY2-5Yu-G*h?HJ#S#ah2NKQl9b z?<*~s+Sbw0t@6WIEcvHeR;!g?wv3LM`P{@)&gi*F#r%}Lk(=_t>1ne{6hXAW1Rvj+ zwwR>ThJCHB?eh2LPjluw8dXZkPxoxhdiXP`541-Ts#%??iHNbb0Ja z%X{6$?zHOM4NYsduN!IWZ>}w8WF`hViVb>^^g8 zQ+)go!LD)>kHCV$xhNp^edqh^3a|%eOP?DT4*17dNkPwM9Eg6a`2aknfwxx zlSjF#x#VppX&O91b`UCt9wbZjxM#oIG474j( z9&3-J$Gk+2)QuxeB~sG3&8x37#Uw~ld`#7}bD5KkyH;vrBO~LZAZ=&gUQ$w2RI-E~ zMHKjCc&TEY&a~d}68L>~C;Pk6o1fchq~{Ul12&>Zq1!0qPvoL%NPZOk=J|WCytDd| zSMR;}Y)v@~R-C!4i<4e;pVf8#vL{Sl^{FYcANZWAD*q-xbbl+LxV3NlN*>v%Ae; ziSsY*Szg!H-QLi>X524dJy=##|7u}(Uv+o$HqE~@ajf6f7ni)uPV$ZO9>t%?s}UOA zNUAVJY_LO(lP!^^m}kQIM|_Mpj%{`HCLcYT-0SFDwMzc6)A>PcYD`ROY;$+_5hvtq zmDvY%b=F+9dG!8On`?lC&5-whk))CC`@2RmOV`|A*^v&M5)28zO28%~l&D-k&nAO= z$||S#?VDb|$2>7uSyECtIAPvHGCpqmnwwBgYNBs6QLaf`)wgE*Qg(Z0YuQa@Etn_C z7_u8uAlnQyPsQh@=prV|5l*63K{*e}VrW9U{(VfZPnqf^xrJ9_e$9NQKXp3A6AP-$ zEOOT1GgrCTqv;)$_pfOl>AJsrM3Uru2^+-V{Yd564HUo03))V~Ofz&DWO-6H2iItr zrNe4BVBzuXPr`HsnTON+u+YGO=h|zpZEAXY2LI%Dop}W& z_Oi***kU?jlFgvxt0OCQ9G|^`H7nH+Qynrz4-$}NH)$^_qXOMn7CB7~0|XkkkV zWa?RCX*Ftt;FYyxNfusjeg6bA-zk5J@%EdGi_P@XFHTF(%1TdLJiCTaElVc+!I(XW z_dS9!7lCH;Xy1ymKe(|;NCgX~=pE;QqUZM9%tzSL^`oOsXWl#zW=zr26{gv@SFUp# zRmuGSzNwfDLUP7`zAeW=Cq~Rh2CNS+e z1;j@*$>l7pHrpSB;HZ4USvVaknejMbu6af`K@ZIu-eZq)g=1b0$viA1^L@f9gXM!A z8PY#_0m-OhK{+;^LF9@d%g4t*NOD*9b(-zXjg5Q$1RoJR+i5J@JgV2zi7@$RoP9&I znqt*r|C-B*pjE48^92mq&$ViM%u%a4ZO)>+>#dEu*Nt2lXO8W!w>k5(4>q=4x@pa> zva*t+Wz~6E7Ms2NN@ImCr=tFPo3q@IYmO~T8gTeVdpwo)a>L@17-Pz4Q{SpV)>76S zUxtmTf|^CKEtOSW3C4Iwah@$9t_{+ZBCe^Z{zaB5^o34vt&)DH@-7|ufRJl$yylvt zk3as?cbx?VPN%zxU1qxF7Sa}GWDED2ZvJBYSP*QPGly z;Y(Y^#LdN#@e+%TD!jO9xQY3ie9`gnofk#L1B&gLx{So+EdK1n%)*THyidcQn7;7Y&je$fwK-dY}Aq!-0;2UzIWiT{oa?pcg2mY zs#|`h^B=7KvB%z$A9?C2K1*N<{6&~b68i>ZWYD>kG%Ev}!B>XMkEe_FmsK{U<-dRX z*KV$mKUzFgI}?>0_w|JZ~v`$z+x-oIQ}c!l{9(6BGoVZGFzZCqECz<8Yyx zj#&Z^`L3O6b#~Dq)-I<~e9$&UX{u@tp6a|H{u@l9#t6H=p*oFo9g? zxH!S{cy<=47ymDRdH23M2c(7@H|(Rbf-IsJ*CoL_)W4Xy@M>WZiU&|(dEK{7CaQlwzKm&UI~hyr{Wo{DzGx$^6m2G^mkkHnID%#N_ zV~I2-<-caKa_=T*alyT7XEX`60?&lQIo)CLl$3ZZWi2HoEpd(0?N#d>tWy4!l#+^i z0l3;{^j1`Ojp7~i7{K}zVbmC`Bc6at9yogxHNM{Wv){_Hbk(k>hGs7TzqbH7s$M|! z2I+=5^YfdbJHdcZP1FcA>Y)At1|GAtZ)_~~SnAtqCz{=BYTN4ZM6||fuiNKy(~BMo z?`Ut2!><*+R_qC{Y^YyO&sJ|(nV`{Hdn=kJYM1pmn|gYhoIPxuN<-6&u!nh2J?c!t z9(LAol0*R!E5YeZgY>6RE@Q%6UW$ywMz2`$V$wX?LdGOIYF%k?e2r)TF>+0;%P zTPt&Q>6~V~1Bxy{p#c>6^J``V3XLg-9!z>X14{@2;bSwCb9HT$KdRM=;j@-?j&0tx ze#7ov8%i4ry-iJCZ(M0lbxBp1>nHi{-knpqQ#*G~$v>Og$#&|RN^>a<4R3%zQ2jgL z@*vHJU_4-Za2&vP7BM%m_}~UQ*GC~K@5Y9eT@#fJv=DXW6FsXMraM}Ewbg3|2C*v5 zQ*}c_YpQI%I1qZvw%n4`R#SD8tE#jmr8swcD;U7nUT5h@D=D08a12#d4LR!97M7%S zl-0EpMbKK2BmzZ%D~(oRDhZa;>V+_($K%urfn;8_15Z_JMQ&7=z%HEX;8nk`p4-KqXKvIomLei%XR()gd~P3mgEgWB z_5g3W0WH7g*F-8UhjG_){F*4G<$7H62ycm2TCU|iQ$KJPcs~cx@&dmGa*4Oxh`U5T zu}aIKxy|f%tQL1c2hIKq@Epc9&+^eB#ra)Vqvpu->`4K$d^y1kl)b@zCSc|*H=yO$ z{2IW_TMpx{=lC^%nYUbzYaZb(fSI>k%X_AN05fknh?W=lHGrA7+=#nGKY*FH9Ktof z<6{A4-f|e%Jj+`EGjF+?Ww7T_g(VtzzLg+GcJ&*SQ3@>6_Z#s2*ZlWL{{1kne~$ki z#lK&V-yh-Mqxtu1d4F`jmVZBp?=SG*WBB(QalN=dmVZBl-+#yZkK^ADk=@ACV*=v8X9mg2?sj&@zvwQ3dKM%{Jgms#rS zESHtnfrgLc#PJ&%7g-Z&(7K8>as24DJJ;-B2Ry88xT{0H%!4a$z?Hwo=&&c!>8bg%={t3Q)JNw`RUhpxVsW7I`+B7BBIK)do} zN1mIO@+i(V=k<`XEZhSz|zYKN@6SX-F>Ti%r=|3)NW&Yhg);(xj%n0J*S|r zxpmlGZ8TQX3lQIk`4P@Q>1kw0DEKWQQk0g~TgwX&g= zs{^oz+(a=g<;-iS!9WjY2|#D*8f~aBTZh}dBQ{HA!>Vp`ZLOJJRFlzAS;_WxHB>Z3 zM#k8R>>iK3$QBbB*;LWc)l{m}mD20E;=H_)5|qdy%p3-E&jKEfnKV34QFzNDU`hfz z6pp_hC^W>FOPafvcXhAG%-j-VuvV2KA%`7!GJUbG-_g7QH1&((+TD275{VQacpj`X&TM7Bp(7nju}XVuqx23K~K*3_8IHINiTtv7ZL zw>y^TZRNh^wvK*J>rit5&lvRzjQUi_r~$pF)*uyYI)79{>+;CCBX3k14OIvY@h74Mbq=Hj*EvCnjuyTuhgdS?C&}9- z5jO!+l?RqkjU3>Ir{@MG*LiFRg*#LW0tK^8h1aBHRT@gFa?-9SGNOETnLRuGiURXU zTiZxU-r@9YJKL*kG!~R6Hpb+pouUoXSk+?tl+eG4XEZB4u1isqH8dpXQY=fp90K>MO*{SKLg$uvPH!^UsrB! zKpd!YhMCT{c%}?pvBo`m zJ;Xt=Q);iRYcDTd9dFLuGW+(f$kce3cX$&vRcS1DdwOXml}xp{Tr~yGEL)ruUBmvr zn*Y^i#MOG6))n#Zf-4Ei^RUH^eydfd;R$q#z$sFn`&t zuWWL(d7tAWD?_oP4VBsX`Ptd|_02Vo!PJJz$_9BG0RZ|q*XmRBKj;B=Aiv^m*jV3H zY^+V7_^-0V>{pu3FJd8d4i0wG%T!)&!iyd5_ukmv-`{?tx4*fNtlorEw>2ZS zie)BNE#H>y$SbrL6xa)M9XTjG^<3#Pi*2L^FVrlKMf zy=1@oiF$h$BVC5x5)k=OdlQm^YA9(~k=oFVX3@q)bvZ>vIr(`7MLDlXn%UXci*j;`bU8VCaepuFFBA6z$7u@o zl>7g)Xkbwrzdfg@Aa90lmp0J-`kZXIMN-7&82=rNpH5y9$|490gU*i#g3$&FdUEd3 zKUd@K>GAB}@7++3GKx(>$nK*vOvCmyE%mEvHREA0?!b=I%Yc!O5sZ&ua}X{mPnZq| znynPQ`!QCpd;g)_LQJ1Vnn4ht!H@36V3j&63!EIPQmIz9niTF3W zi@igXS|YHj*sHd-HX9zT^>6-ex3wX^usC9aWRo^?YShEGua9WQUB(K@=*X9{vT$|- z5!@_1uf~BUqa%wNL;5857BYt7JVspT$U?ym_AO6r&SI@4QLl^j$WOL=;tKVN7VYBf zI9*yzRnn&1%}M1INeB$(XW&(-jU9@ONlh*9ZRog|Ba&Z$G2*}?UNt!$v60L<#AkjSX>!}^~YWsf3d?K9>=x+-s)ppe5~IG zn%R#l|9~s$d>}t@fdFU9e*b-b^mlfMUbG(5pe#CFX3$hzg$z!O$g?A>fTD@1eD95X znt2qb4W|VoouSlzlpdhE?Q2Q{L|2*MPhhn@D&_gNM#)dA%<;m%*RK6HQ8=*)omyK$ zdf7^_1$o@VrpD#lIL7Av8buoF zyYg-0Y)bshBH|Ej??eBPZ9I-fju0UZ8Ku$s8C$Q}UtRioX|>Ze;j7a4#d%v`(sFYsvsQL-Boy%ddbC0t7>B+^io28*;FN=-H zQ(A7vvy559kLRvp??A@^)+pi%%KuNKS}WLGWA9vl+ilm2gW7uvpE)se;+ewbNkjYl zXZj8dVOy3(?7-c(i@USzE~?god|~CBzz$KS3|;KOO$YjB`u7hdEf?J=IAo`RVIjQ@ z2uG2t3OphD#0?+F8=Nlz%3uoM;0_L0z!-X_ak(WbDaz#8vBP7+2g~wClp(Q2CPfx4 zZFcn);e)NRe5Be+y0w@HTPw*^fEhseDaWC~OUe20v>S`2?qeG6`>>VaPmit}+*V_YzGE~frG@_4t4h64JXNx|BG=m)DQ@xAs5C4^xJ7RSuSL)L9 z3v!F4TbH!-_j&T_Y?)6xSGcpC?Y?c}$*mKOFWbA)a*V}!O9(HDOa;7D9Ui+T)xf-F zln@{GE3J$*<@harEXyc<#IJj}l;!X;b+?#YB7QDX&^2cc=qP16>|Vb@pv3PGncJe^GcL)II6&rKXy6i7EPP8VEZ}YV~v)wD4PiNZd@;rV0v{!Ia z;oVryy=t)}jgn}?-kH3?=euEY7zG~pByYNV{mh2DHYK0^zVxT!V-qvuSVM{P?~j0G z-a}T3Qe?>}LeRz?Q^$P1V~M!myGQf0FgU)oAeUa%Z#U5 zwbI{+Sp3!E@AFbCuDG6k7ZGi;p*BIpMr$^0q;XH9bsVjrTLzDTv6%H*G@(o|W+K}H z90Z9^JcM1_jBL(se%ZZB0&7jZv4Ug5Tst(5Muj zGiusrABPTsz4LqU!s#!eijIcUc6kJJ3&WTcjr?NvBO(J|@XE6nMBVb~>=&{&*?V8? zGEU&1XPJK9ZxK-;GpBO~*;Jo{W=~kpIwuxEp8FTW$!uHXW1lv&i|z;uUdTJX@T7dc zp0G&Aq0gJg+}zn%tgKtC?3qWgvPMnC?7#R5SNxh}Vk>@cr{5`3hzQI1=RvN4;u(-d z=4>3cfXxk_ty?_?e6S2R?$}XTBgI8}-OU}~_nOKbGyZd(1JBZ^K5nlo-mq)~1Rrw< zy|XWI$h?0g$bO*|vysjI@D%IkO2F4b(#={%Rw$=MM0` zzpubz&;SOylZt6ZM4h#u{kwp15%wre5lwX3$!;$Z^0&kuMS_H^b0r)X9@`MdnIX`~ zNnJSH#%j}aF|kD{2*7{$vtI{dnCXE}?7(>fa-QWk$Kt?DLhRT4udJ2-WLDyx>@M}w z6X%WCMalyaYZ0-lg0@c*aTAJVC{qzp^pMyW+0Lk|clMcLi={Z^g|c+)1T^r|mXhMK zvf>g8vy_$4?tQ#ViC^)?*!lAV97OR1Er&Y+E+sRkqNm%C9d?hW) z3S0OJKZ139Q)#D_HEJxhvREeztvLF85^<+SPD>TDZ{rulqkcgve?fqoD$X3@cUH{8 z&c*NEW6%vW3bIST225}cCFzK&sTom-DbIcynSAEM$mH2IAA3KhnSSW=-6wr*b^|h? z&)f}e)PNhseH7Xt4B}Nne-Yt1b9eOE={v>iY%W5c^0(O8-d@?)+xsg1kl#84xQUC( zIowq;>=3|J`T)&`+Lwm4mpw=AqnPK9L_4R@Xie4c(SGJLv?KQcn5jCmhtIQ0<*8Qz zuRMU?vA2atXdL7R@?ed2vMD&YUw}@Jq%V$?OgOChqq8rZhajIVj>}v`IC}Ryga;^Z zMHF_CW)YyYDNxz~dNBt^{rI|!>sRc46blQJtCp-7h%pR(;H zXM={cS^2rM_zjfry*%c0y9j;VUffh7MM(pWdnxwCXT)R7ist#RSXN@t^8x;fd{Gg5 zP;L(ZEY%@Oy#PFFY%6m1H{dJsYbm;Eg`|i$pNxu{xAODjzRVILxJZbdy+3T_!z01$ zyQ(0QUsQ$L<>vqz0rk>q=r#rKKJRZh=BtUn4bIXeuOAD&|Z zd5xltApXUNMIW+sd{E&H?gr2&>v^S9jbBi~%-xE8!PbZ>s-P3~d84S2<-o=eANK67^9&og!{RRLIy8#fzBQ|x zKWke6gk1m5mis1e-=t+&3?+gG{o0Vp8w9X zLs7>+dpOE_`UeZ#$>dDj`7*{#1`ZjjJi*>n{R#YtTRc*;JixePp!@&HdlUG!ifeyd zb7fh!6FZh=S>A0~mgU%z7s;}`$h+e;u@z@?l-+R_C&7?}5Vo{H*xH6Y1WHnx7ATOG zJkcXiuZ^g4;~HN6pJlE(_;S??1m%=eUPup1=_lYNmi=k8gUdPL8<R}NqB?FnE?vgxQ1HQgv-_QVLsAIC7HwYn>@6pL$d{}$Yj;XT1l zcDo*5!mCLYzG+So-{exRHmsL>iI*hhYvtKUwh!*F zr7{zvFb3II;&k@a=%*3(3|ZuNMp!Dq{YMibYNmWL7D@i6+P!#9-b%0srvqlNznm+5 z9N#}#YmlGuH@-~@V;E#W>ZQ^Il;1%Cv+3lgLVfMHixd)1{b5*%OEkxa)p%%$%VWCiZ0~RcU zn|+4!zk0>@S=N0n7e?1PdT*TeEZD5K0=_pS_{?b0Xwq)QMy`+6j*f0cjMN{*GtL(` zVsavfuVg3T@R_XyoC%aeAMbnd#Cu+55H~AV!|XNecx`cut?~tG7(xxdQecB_YJeTY zUb7_(OUmgc+N0Lj)Uxm;YYS4@b7s(}NwvFF<_1U5nZ+4E?yr6zwq9G}9 zVMD*UGP|K{e%-toiG~>q#4%fKUS6%u=CfLTn-?X_n31rce!jUTC$~BkWfKx|P%^bD zC#O1fL8H}IP(bDA9X;;=4idF%v3eHstaXT&wj&n8Y>>8>AbttsVMbOLy9uc|c{mQ# zSO~AB0{Jk>L1kRNQ3%vlW^~V8J>NHPkOj(}g`NJP{_@IF$8?{gywSI7XUx#T@_Ids zjxlfQ^skS(bZ5-^zS3I#zvIorU45HkcfJziD|0j@M0K%vePwY)do1_~@&n$_bDq$? z))TozvvJ(nqhM)TjS&y-nJUe&aixtBHi7M0qv-EoB-W!5TlzAw?4YI8M|S1l}K(QR$lx3%TAB^9S4 z2n7jRi>)nXd7g|CnkSS;hB9MGCx;>W6nmD*i!WR9tUUV#_ICp#Wa1n4grb1jRgw&!v|7E5jVxbAS<#w)j56h z$hmBs<$wqV5D(DMo$9hCXJsYh1T+jHb^v;78tH$lJC$vLQ*iyD(NmpP+)z0E%Fp4Fb5QirRhthm#Y;AiDhoDyt5lx3gdg)AP#2=f>1W5BtQCOv;K&HecK0~7y1 zpT8{YE*r$__1@m6pjaJk{aaSZ~q^M-w$P*XgbQg2+dS89)B1`D*-h-p;CpmhN zG8@kZAC&5~fjThfwe&hUHxvSaK;abp@ZjT*3ojo!6nuSBmPlM@Hn`5EyeDp~SlG+IGU#CB(H0$>8EX;u7(CzVfD1!N>j6m> zHqLCa;VA=i;W;w%O0+~MrZ)D1SkX~qbLC}ZC!13jwAV_PDQOT}pKYscPq*9CGc(%S zYC7aA)S88PSPPpf*;X)?I89b&X35bO)wOKW=2qcBcpxo!u`}h1zbRkbpO=+wwPt7K zDW77Saz%Z)b?iBK(w9L}0^d@_eaYb?<_Y@3T}###6|GycX@|$N%y+&HqH5wN<_FlvZ#89Mw$G|{v zdas=s^bE#o@ekj;`YQYCHJ30%;!yA=X8h^XPl*?@%MWv@LGwAJhU+D%L313OOQRV? zeJZOtXY5npimc|xk$L*x;|x!iv*ohY9CmUtGeuaz8CB9AXuEN$n;G<158x(abuJp85SfZ&@ z($$o-HJe+qADooD(2qjQsxr)DU|x!TBCnU*RM-?a5@}IQH)KAg;X9xK`3{!aCVu1#>|#%e^$`28l|j(# z(i}j{)8By!@)M}&g+v9!)l0FN&ma_Pz7mXyc}s0rHErOr-v@s!;_`+p4?-N5w;Utw zHN-?NhJa&9JAf(ZF}3SVpYO~rl89YhGXvc$>#M59#;ULWP6>tn-saoXG1$4$*M@k~ z!8yy?_SRJvR@Sv;n6u3p2np1}pOz*J2sHU9R1pn_E!uvi$jy{K@`h z&%xKZIrh&cTwr{uhc)OS#u@U|xc;G^km*v;Nfui_)X*?gkDUuI*&A)pSXyk2c2?e2 zy}JI|`qkBbe3|Xf#&^IQ)7+tX1iMz>qcu*E(q#1Dg8~kKqV&p!hRWM+%gO)n!~C4v z9%-m-u557T=NO6%Ir&b|$%mRWwl$1{h;dWl0M_0RKA2PPbndv%lOh4|!efKIxguJn z+=+&B*?NW)1VjNhVQ1~rTr1Fr*VvnwQ~U$mM(0;m-BneErxsvlYlRL@-I#BjFYy(_ zVp@Pu<38!7@;{+wr)DeU0E-l>MciK%bZlj^Pm>lpBCBIdHw9|l!|vJuvJpMMY3Z=? zKif_lr^{#dEEPxjvDmnlc<&H+CGA@v4-8xoD~)Wq%3l#BW1NL;lI@wEn4gqf&Ku;m(j((dg6lF zL-B?v93QlXCMr@5t*GcXn-^EG{q(FK8I)s$mjQhtMnzhCg%Int=E>;*>>=w7yutqV zT5DN>H6q+n$=6nwF9ox;P~HaT8+5k4rtYY91CU*q>>o zgajhd7Ks_MpegNgvLbc{IuQ*GsQH1G?yeTk9AnC~!nE8xYi?^-XRFtrWRA0?=2)${ z5BIdSBj-veRY7C(k_FeIXWxAyc=q2K zpAdHaZS&#J4}XqvjMIE9w6eQ77sH5#V1tZm!w~2sIhX@9ROs~<&aA9t4~iSvjUu4P zNyY19{^iUb7MmbPCbsvcO?6homR?($pbgw^_A9~ zzrc<_^Mx{}N({m&Nsqh!Qrl&h_f6LO~u* z0DBb@#Waif@7o|j%r5v^{Dg#xFY`a={hM-iqn>|-o?~sHY=Umn7$TaZHk|m7b1_;K zDW(K>u_{6+9XqyHd?jjw|HOxYsR=}CJ~e&XY#y6Q2Q>nOuWJHTR=XTBeb$saL7#}u zCjRie6o|koXVkWvGEHeI=Fr8Vqk~67(FU|Suoz*0?FKF;NRG5~3b^FbqWXHu)geY% z6vlsIgqp_wfgbV`7>s;7ICwV9n?Op%j!~S<^&T8O4?nlhY zbh7u^Xpbb1lf%r^ssCXKM+}y@IE>k8f#zHXYgXF3={^0aFfzSHx8`kx7P6IP+F z5?0MR^*=Zt?Ge^>Br%WIFMg^1ujzxHcNShVEjcR6Fn!NF^*@+j$(Ua~;rT`DmV!xA zgDH&}#m3QuXP?OvEI7(;Y@0Z{it<9!OOSIdksOA}X5;9S&cZ|*by^8guQYkUQ*b!d zx#t$1d+u@9+%8wwT-W;5m8;h;IIrM5{0Wx@Kkb@3w@XM0e!6Pix>b^#;DVfRD_=v3 z`C3ar)A;k?dze88Xo8x+PKn&#-hb8HxmWcs-ZgjbuEkyFw6>npCB3KS=BDE13f)9O zSLiOvyuk0H3}^(c3KTy!C8|q6}Xv zoEksYL*jIasEZ^s^@(#%EXan3&VOj>HlmQEX3UMSP$(EK;L-DpVYtGyj=xD8M?O@`@&6_uh0ux(1I$E*ZhvCH2NJcnebc&@1 z1H2MsO%&)sk`$;SvaPhi8lHss3d#15g_w3C(P>NEkizL{GX3#fHnTxIx|t;XDW&~E zvZmgD5|JMk0oa~vz^}BJeOXf5463ABsH1H{Zf>$&D}}^dExt5sT}{opStQxDbau9| z^5DOP816Vgj{nt7j^=^NzK-U?<__^g=n7g*Hd>)LI8`f%ULu_sNX9}$v`2OmD_P;C zR<35{RJHgLE0-EWIx3|JsqVhYfo4b3>dFzEb%(h=krvbmfQc5K4pYLCheEXQo7{SX zp+1*2iZlj@qlQ%WGOp;91PyUwvgBwDGULDr1F~c``|lF>B~BYG^ZQu?Gj55UE(i%R zc)D%uX?avFbUG}JDH)60uF1wyKjj^Hlqa)M zlz1?L<8fY;4g7ofn98++pHIT=q*{MCp3_OSMlv~C5t}qCAX|Azo>j{POv9zB6^BWr zIt)+Q_&rrank(6-LIRhA?OxbXU`a9S?fcwUUFE*|I`?%~S6p>v#Z_0kuez?{x~nR# z#&`|FIP#S+fGe&E(#lFE3q`h3Yl4I9+L7@)Rqfr~cPI*=xKX(xijt#*iM#Ukn)tn} z?aHoOPA#C`(26MYPRT{+0mgK`XRSa(xB z6+z!Jrw_^4O%c>beNk2ltP@1h5i?fmlxnW}{o}O8Now$npAQj2K5B>nfl~zO6Ezqm zK)`(+Ffx<5#BFMmWo=o~ZUbGkVdq&AxMM+79!6M?cg@(DsAW*|YxWiU0qHE@4d@B` zvG`kd?XNHKAHm%=&2b@#JxkJocgl6DZPK-9Y|;e>b^6(S9(6++zFK1QKQ#Y=xo5@1 zYoiikx3c-{a`Aj7B-`TRY{?>7TCAG=f`@$rSR5Lkq|Zu3X?F7zog_q%nr%%G>@L_E z?OgbV{G!ahJY)-iUjDP%j@fnYHiK4|<<76|=&q;hsBBkeR=z2Y<)k=r>~@w$Ca_E! zeKF0lzpJjhuFGAP;>?w=OeMKKb8cqz{EC@*-Lx?qZ~XuCbZ(8)K+2YO)nOgtcCwR5 z*%VvB9BS#hVRcQ-YGm8P`=8j~+C_7OZ0+|AH8ta`m-Wp}L%vJ6%@SvT!gfw4oCB*~ zHCBCU85l+s#7%_7UyO$OIkn96nOa_==hUJSo>S!b{}*yI1MSWR?NgQv#m7maJ2$8* zzD>!qDf=~rM())N{s;LqF&kW%zjR`6v;Y$_dZV^|!(9sH6*_>`Fx)1@v;{^EYJGiu zlJy&9y=a?ax4?j=wtWKe+67kydcj#ZCZszsFrjcex@Cs&LX_S z<|0{;5oSQgts?=3y9~ZZ(0`;K8%JF)cdc5~I)^1Bl=lRRt|)G7EIw7_VWvRq`;8{{bc@uv9T+KV%c^CFA{08P|1bjWtVpb+{>HJH4pD!C43r8>e(?5 ztd-Y<@*RY2viel=+y#cQ&dZk}`2cK1pHhrjKT;O-1e0iFWzPimpkR#gyb}@Up7^639IlCbpPBTXsbUwbz3j2mR*Z z9}i$4G>9w`x^QY6#S>GMel2)0W;0pEJO$jWGHy_!Q(%`yDq6Z{9(OM}C4qg}B_^=7 zjooZ^BQk6>!bg{rWo1_JIKE=t_zrL6xWkJkzOJ*riE@DAZI(5sDJMT4Z&rrvq{`Q= z^bP+yUlToc0`P8;;VtIT##qU$fEYm|VDJ*hj7~8VjC{f8!6jTjBi0%n4U2%P9QtA( z*B8mg39hSSkF35l<&Na{F01c58G}Im(6@Di<|4 z=pP(pl_@k0ujTfs}KQs-E4VGmbDUK)~EHLEhV)w?ZzHMmeHkC)qJ8KR1d-a~v z0>4-Lh4W}GA(jGlyb9`pb(EwCqz;Iz$;)0?FrhBL|KP;hqQIBF7INW}kgR;A08%Bj zsjn?le9vCwXJ?g^h&Ra>%rfDdrOy0(C%wd5lq=V~~pPXng8V!lbH2dWoR`B^1b8dyz7v8G7fMLkuTuzJBJ7o#qCB#fLYxMPvj1%sq z7!yxKeUr-e*<`sMFWIBMabi7L^1Fq**c-GH0m8YPa&em}hAQ$BQSzWJI&y^qg+rJA zAOCp6jw=m?@%ps*MH_acDp&jVZCDhS9vfe1NZp}aUAd!sWMscqh>cE5JVW^$85xdG z)5kLHe&v%Wdn;_*3df|elFg0#$;uvBN$F3>y5GBq%7WW+ZVvh-ep%_pesYE?!CYeRvcW z^QeLci^wQk=jS*3X|pn1FhH(0;|& z2gG0U-a`K6S(e;v@iEH!5ru8skd9@ja2NdpHUQ7wW3cI`Q{E*av3~FXz7|qu3x?H2 z=nvi%V6oU|_b<96E1%@;`%hj@%IAr)r2VLMDO$QE2ArHCYeh#Zo0}`?CFi_n`JCd} zPw4n}dU-6FnRz&$*8)J0UF!(4Ydyv1Klh?48k@Ri=8$u3d8|&S6|W;o&aFcJFTSFA zB+LC+MWH7E50JMZ^O#9M)4as&QWtc@%KTPPO|d;#VN;6Mv!q|ZK! zFZvkpa|nWePbhougW@;-YE1j=6Za>ozEQnc7ee)_RuZMo7vFlT|E;$|RsHj0|GV${ zKL!@9ngBRf4Pn|V+cT_^6(bPe-wf% zcrbEs%M0c}AM;6;3yd;xH>qB!n1SlemJ0HEsj!&KEhv=H?RC)H%g8Q6UN*PajMN_X z_p|3L$;w(XC%g0Y)s@SZA=-n_f%(wS8~GgI78^cVs2f@*f5|kjSXhN9pHKAreAn_n zqCuY#Kf2V!PQMSBW?vZYn8ZH&aS}z?Kahc_bWh z*e2N6v&XMW=Z;&G#&I?BzL1K82%kHN*9GqqVqkC?SBeo$AqJ1%?++0BqvFJsXs18< zM9G?@*q4d?=-1G~t#sAlSO9LN8xBVVOo)0@%Q0&186nd+#z(s;;cx7ICFD(PQ?8Ux zK-i=yfL3`XiDqFu6*Ome5G%~w!eQk;rV1G*|L zHp^G1@m=wZ`c8x)q?)or7e_-^p^Hhrq7jgFU04RlR;AYQiON2EV#x*Z#wcA<{P{}{ zJ&Ta7*F{4`fJaq)j={zBqYw%{bb^ zT4J?hHjD)VV-kG@@8T3Ftx1nd{i3sO>mRMGV{Q?U4y(5t}j1klkgMKB$Qgab# zQ#viA1^ea}Yu23FX5;;#4hay%uB!MKsJ*`27PU^W@+M*+Nb7)UC5hXjlZn zx9>5-UYe=e9%j`Q;!{&MTf$z+6m9>NR!@fKg{*PvCOMuXdkMYiR9Hs3)sP=y%HD2p zKX7l^gZETDc%SEe zkj*MN;Sr`qs_`H0Psr2<_XrzeGpGM_<9mpvVc2{93Aaq*X`|O-&_f~fB9{~N#@W5> z;(0oqcw;pij>}ILe$XpkFps4t=1miK<10PIG9<%-+ylQ4o~A7vs{B3S~FB=~j7@z0BY6aUIvhJL_4p=Z5t=$o0BVdM;EgJm|Gk(?hVUSG{N z>U8tO@Ae9MHe4<4o|c!$(&vd65Zr~ZTu|gO_1c8s92n^-x(YA|W#S1j_((!>lwU|m zWKA31eQ)DWr)S0r8-jacGAO4otpYQpmTh{A!3*o29}&I9(`|VUGlA z_euzr3CYCi3b^$mEiqwMAt|CSAo-wpqKe~KTO*U!1|qf5XMpeh8#tZC!4uQNo{;uA za7hSz9FRHD<2IYD9zDD;@;-;S*)>*2mdR|$%3QGP_ig?;?)E%KW=d*Oa?Ty)b=_u* z#cWyG({}VHE1T##H=R!Gh=ZrI2R3X$w-f*KCzqqj*Xji)OVCU9x=V&J{Z^zIfG@EKWRj<;sgc-i7CP?b_wvwQJnd zHnPk>q$R_R1SR|I;)_>aDIQ~SSFXDFV$`#9`N#Z8szd!W%^A6EiscM9j7_BL_w8G^ zLvHnAxyA^{NXGKE!>pIc+b z!K`l6@b%a4YrEz8>-}uQrE7lh{k1#An<#&-f*F+eqLkbQ641o;u^V{xmxyPqVC#7U zK!=o-R&If!{|wli1dN&u)?s$x_1E9hw(t7ug#y2L)6TWu|G}C|*#?qLHQ3#aQwJe? zC!s&kAaoGj$@+z!5epJp7c2-q&KBuR)7Grozkk)*nB*Ao?n~IU`-SBA9U~(b#U){z z2SGaw-u1dBmuLn^1g-NZQ;KCH8}^d=+h%8HOHKVw>c%iZUA4Xq0WN-9OG!yd-&xow_Z~4KWXqDir>;xs=~=1KTY68yb+3<{B%dDCF&dioi*&;G_g2tS(++ z;~YXvNVA;)*5AaRE+Qy5Y!F}Da5uXPaQ8Pz)o_rnxZ)_FCu^Mog$`B?etLn8k|S$j zr_eJG0wOst_rI$ntV#{f!(B7y` zQay+OK^>%a;Ve#_*KQ&l**~VoKE_igo_!Vv)aScyw`823VYyv=|FOp&-Ly}lpRd7N z7ZQD8oz>Y)tXTXraG4eN_0TuRhe-ZG_B8TclMM@Fu7!GQ!3<(JJjt$)JIH=@@L=%O zW5-tP*&}=(2!nkTX-1-a=`9kii zsfmlOyZ_wvGcseMn0A;6QMxqa&=G=EhrL@p*i|J-{hkUWI+RS{vZf!ZR?l*^@(u+78L9-&7?I3?HBpV}FEI{~~yifC+^g z(Lpa5B!~&LA=-TBOE0dwoNV+n6KTI?;!LvPe-BwA?wFpgj};*2vDoPJ8MlwCjmu>d z(5h$Mye{+_J2xdS!1sn;da;{V#{nnIdL(q` zaKxR@keO6|hOJlM5q1YRDR=mJ@uAwZ)uYdb?$Eyd@SV3u^OQTip$TrX1ksp(dpa2vYit#eWSN;QGpEj+pfB^J>t$&syk1q@8C>?aBt$3J5;aI8-~&0`!VH~1e6Nj14oOG z>=Vj{Mu^wL@gvnJ-3iyI;0_zAB6=0Zs@xVh7*R7Ff_xB2AErNEHM( z@di{Q8mforje5)L!MPFPb_hC-+2@4;e7jrNO(Rde3in#w$apAt?_@}Wck$)w}_^CeDu+_kqiw z*cbZ$iSL9=U+@E;^7FrWsr`-8s?T@s|LP^38ljpS|9h15XU+SXziK|#e5U!Q=3kl< znlacf^lTcN!HiJUGguBKB4Wj?lmc*BEkfyASUZkm>1FfSBDRFBV5`|WhO;f$R<@m8 zz`o0NvMX?I*EQ@0b`#r+BNy$q%0Xb$IfJYM0xi z#%YqCNLEeI&qEOK3x?p!i9hU}iQg&T2>r^{q@Tj>NxxRU{C|4;B#=+u7`X&)Bo)M^8f1g6ga3;3MowfDdy7mSWiNKKzis}y!n6fA9c-W0$zG$qS8N; z{2c!yeR)v+We(-y_W#m_@;J>?(vH4{UM%fsoZz>J@J&QSP?LQk^c?N7R4h4D?1PM` zNxkBm15 z8mrfO`L=$1ZOt=Al%d!yyBA@N`nb5VDEySg*&Wupw62*myVB~c*V`NpTYO?(Qj*0G zA8)WECFLc?4#-7VK0{G;52kcQ{d>cnNd zrQe0?wT;6rqCroEUB358<;|TE&t2Zd9?xQL^w@xVjLx`Y~Ax zY_S4V;OAK-t90SbD?S&RWpp3fD@wq&2WPfaN__sKk3N!%e)Q4bD4I^@Fj9HgSt(DB zR(Xn2<*1%~L-`ji@y7)@s_$jAl|hjq$WnPd-g{f3 z^cgb-24-aFqx6|GR;|YIdQ~)P_=||Ldsx zXtqF{4hsZto_2kz@qqDDFoEn5RGh>1u1l#n_<_(Ge4JB}kFR-v#?DwAkr2KJ-ioHU zotV}`+(Z5>VZbG95}yxI-fsHd#t>pDJ}+#-jO2I9F-rUi8)InpR$-tVnph>LN2MiL zC(xG7qUFs6|H__upSS5^PxAgsvs)l52HMrxYyjwG_8YO01MvR)?}w#P@zpBhU)YhjeGX& zIl?M|YWdC!QtMuWZV!?(6IeveBb=J?1ajMeSodIKI)7r9Oy{`63^wGVdLDOP*@L<{ zo%2=z3EDyCN;Cxscx#0ZQlF@d)E<>F=)g75sQBfB4=P=vGD_PhW7AsnUMsba9{*y5 zRld&a2}2?GTF)V=EPPPzG&s&baNhPNz(al=MD&5S;p#*1AENo-9WfzUTEYSGl>;n6 z{8G3GWnUf)?iMx;2FtV+gM+{{uu9{s!i+y zf*l?6gWBDL2d*BZ7SGb&JUIFSZ;i(9ADTBgl$?)Jm>YR*l7)+c7YP@!+Xn|n01Ns; zrJ`1%FR@Zz;E&-YZIn0Cz?Kglpa+5%9T;S{3y%lyqUT1CQ!a>{kn}Wria(8+i&!T- zAw8GOwhW3z2L|69#Fp<1mGT$?r8Sg?PZd3|;oWW8Gl+zcUk)C4Q+w&?JA)&mHr>VY1Q_dOWlLEcLm8w@W2gHhwS(Gu*Dn3br+Z7>on3M48Rq{rpfnbaqC zJCEw7mgW6;dkzQ~3v?fFqfO!EU>UkA(G*G}3R!}a_U-|~*W{u^7`&temr^oRD|?Ms z3h*Q96gHuxUak`mjJzpa^rq6b((bh=gV`drJMt#GofjB=A#@*>-4I-aIh08lbn#L^ z)?K`$W(5CJYrH0cYlPj7rZ_~XNxF|Q<~30*yo?K^&r1Pst$XcORe(K zK%G)t!SicUFEJM}vyStL`y~Lnbz~$sp!8!odrgK8l#VhWSt>Dli;Vwk73eISKCD#c z%@OM7ts}zsNAKk|X^!)LDm77SMEdCHT54)sKM@WP)QT&R(eKdId7thF{ir`U>griN^=_4K|pR|+xLj|dsxB`Zp8 z7QR>-6^et>64l`6I&95eB`jk<5?9MK!HD`GMHqPrq8E_UxKWj6k3zc_8ozK~XjcMgQi50uAtga@hoFgog+2V8;FsU&`i=qxS{@m*0fqxI z44lW)*eT;TGV%xxEZQrc$=bw6wGH&n#}RoXY4(K15hv#%RfvLP+~N45S&vY2P&ZoS zlr5Fu&w_8jQ;!n3&u@?3D~%z)i|4@$a2K=E#pmS9)B+d=W*qe$ZE(&*EsRhDe4?pp z2*D<;JPM8BIhoH$&=HRWfW#ze)=RTb;?LAW2@Vb?m6JvbZHx=$oJyUb~+bQw`yRE~)Q-_emH-yc2g%k0nX9LmIHrM zbP4UBK~E|FQ?j*VsAh29p0%uz3R7k)JV$M?Mot@ef{~?;%EJ)jHVK_;Zkjh(v!UI} z8UYYJL9p>r8e1uB!bt~^QT4P--;G=oCdN!wtu~p(i~sb0{<&ZLCyU4D{zCQ+d#6xb z#O^K>bH&_3cDJ}lT?gg{PG-P~2SR4>Wxr6iU;9zvXlfz#MQ#Np&$aQo2ud$g496M$*V} zDMjJz;Q8nRubIYFf{ZvjarMw0X`Mjt!CUE<)?6$R;IUxhSTTs_H-hI=zfR5bInFrV zqJXzL;O!(lpKwS&lk$Afl#EM)Nr9Mnej|9k0!?Vl$PXUk`7#uM_&9j}@-WXgf;JVN zFSQOmv`@Tu^!IG8cpuxxdc+4Miso}b=BvjC6`oI$UYKWd9$=uQkw%)tHfepA=L2x6 z@Tz1k-T~=cwo}bAjHOhDm!SJtT2yn2k2P^Sz>q*tfEk2n4YM=^;pCj3sEG5*Nd7lY z#^ik^3gG&lTQ18!UI|% zIYn6kl(|6^D9;YgfQc_jDcjQnkFe$SZ(MTj-k%127D^ zgDZ?OeW_`TG*rH3O0){jOeI4FNsiI%k?Z7RAeV#I3k_5vxKMpGR!Yy5^@?*T1=a|< zQqKZjt0Wz%=72nx6~ z7vvgjQFflG|aW(86`=}QcXtGBXbT}WK?V~xX*Phg+Foo3x{4d!I19D z%`gNTEGYyV+J+s4ekJ&5oq!bu{JVejLgHDn1qJ0K`w7Wt#J8v*tS6`u%TXB5h`1#= zk#NF)A^Av_0Zz4ZribkG5<03s12^lZXR}`r& zRT~%;A{kT<>cS7~;5@HWIiLfSfD9H*q;*1U;3v%J6fSZ)gBLk5;;m!Hgc3w2r%`+| zvgOFRERoj)o8Xak(rTxZg~p|0`o_~&Qqt$?Tq0;I=dIbaz$XYroeoDu1-+(~E~t63 zW=GrB$9$&?i6+Zq3CV*C#@K&)an({!=i51+729FfaHFT1;yAfvm z;>p@acHCP|1p+)q$XA)kcXJa}pnWZZwvY*8m`;UTO=;jEsNkSvY-GS*gX^&apk63I zd7Q!)^%&l1PbcjB+6N$)OFM2)=?A0?p344Wih5VBr}Y2*I?HO!!s~?mIkn$U_`P!G ziSikO5dIL#qhqHWJyZGo__(V)rV~Asm7SfH^s<(eSo2Ct^VB)sRC{6nCY7q}EGf;C zivDHF&5ED{+3P1|ah}QHBeW0+OrFI#l$l{loUu^KhN-c2qms;lAdtqEkD~R|5ResX z%C^g0Q@4496BD(!gnPWSh<-(TA<92Vuc3*FA5DR95c|=SV3mMs>eH$S)~&k!^l=<# z;G>fa#DCySgH1B2{)hZYX|LM&!3K21f$)k#p2{a$b72H%W=+HXL+oDD@lYjLL$t`M zgp3hJ>@TrB{;uoYC-R+9TYkD_%kZ%9IyhayFx+lO#hL8rq;(ic%W)g&%sc~n%#80I zvbyr0c=xs~Wm~s~syKx60iEm!YH(s)=xi#|++fB*#$6+=^l%fxeGKoh>(F^mh&W_Y z+|Eqv;*)gfG4&1jKgNEHGy^Gt3W7t0!@_YGH1fTE$gF^aWFm7ejKg7Pc0qcoA>PpB zD&$uQiB09>@OT7y@(oGZ*-lsJg3~h5R>+6WX-o2a1)&s01evD2GMhD&)9r8Iz77LmMinT+JbtN$5>S=Mc^tgrs!_UQRTMqJi ztUVATJQD1R36Nv`H_NxhJO}*X2P;!QaBL##M@9s0M!=>^DUTK>EHlzi$O*`iMrbTB zhJ)92gPxWa&mg7zp>#ik^oAw%-$+=!y~QJC0#d~%!25{tlrkD&)ilva%6bSb#HbS( zxWiA0oBVvm#J*ixF%!^Ba=(Xk7B_P~w|5NX$M(kE{WCws2>5@7P5>`UkYR&PYjI1n z*ei{;ff`jn%Zh=o%3r;=535!p{pt`$)1@~ zR&v2kx5ryvQBoVVBJK=8dTW1YeY-9ys;0cOC4Y8v7FP4L{Gy(+LR(o*dSfT$IpXw#o_G$Q)x{Cb|5tpFJ!qlF!WeB@_Yg8zsgB*}1dI}}OWgmaUR{r!rvj=~X` zr*f|#Bo(xkvg$r@Tb{j-EF3%Fp?yQRkFO~dbEHAR&`PFW)zqt9M109!nKdY@THm-l zPC}`|9F$M-wuz~ow3=P8HNAi=QF3-)qINZtQnv|yw|`tK=o;HTv07k=vb<9+`DDl@ z*jXs|iL(<3!z2)6`yZu9!D$FZWox!?UsL9K8$atjp4p2R53{Ik0|VQRA9sqsaEhbI z!Z&|uPtQ`W8%RrX6;9EGfrigdz1&y0@=)okph;1 zX0Hi>T|`83x)$jI0b3p{n3RbQ#oWo!57)GIH>GFgW@TBrELmB(Ia#f9T57ABqMG6s zcE2?5jJOrL8pq3p4oBhP!}R|0OmFmxxD9h(>R#B{(9o&SiPU!aw{2aIk(e5SN7&y_ z3y8gRmM+cX>#%2pvhF}mGeH-j*cZ%U9#L|p_MXvsPVGG0RhPqzr@Y)s7A_$*_|Hlq zKKP*WSOD(n3r)(iQK4t!G2#|Ee$NgDM^hMC3&u-BdKn7lSaxlG{6- zQ`6>-_RY=BoByITRJ&|8iB#l$xTI{)Eo$ zDv&c)i}xxGvd@$>)#6g+>QSXB%-%4TD2EPqbVjW4N&}yd2rt-aJCI0Emgk*|z zmhdgDLV*gn?^aeUenF{tvU;(8hSp9dBdl=byl2HBWObm zWMaMH9jrJ|7?~9iOh)lfQjRt8Z`gg2Bz}sZvR0)=7c0DK)ErEWZyV@X0evFQpArg!&nV5YzbM!aCkp=(vQ@9ru!=(I6j$WX)nNxQHc(=l zwd6I>^JukjgU;txq+~_Kv8aGnh|^`HtVFz(JPdJn5BZa_^zqIVVN-B-iZebsD{1bK z_@&ZVcs9tRtkZgxoI+-;GRiDInEB4Mq$r&+?%mX&_^?v@G~wae;J(ZngP}G<7_1F; z4=VMl$62;-ljcNnl7go+VC8kZ{zIva{fZ|x7oSrqQDH)cPL<^x11S${2D?Ezm_j@g zJTUkhr9Ri9XJX^&bc$yJ;^*Ds9JyMobn?hz1)PKIBhk9cu-=6+A!8kJg?vx_xLvGD zt1OyQP9l{gZD)lZkKLfL>2Zt1YnA4~p3g{3i;fkr7nQ|gtLw1%#ne5sA^0qGGJt}2 zp+5LWL6|8H$;Te;4nC#SUdEC;S@NP0sfQ!qglZ1ct>RgL{lN>l7FMYU{L?8APmCXf zY<~R1E#n3vJetXxJn1}GoN|nXMagW+RWQ9tFD>x0h^F-7Sr@YUHtpEawywHtK~-IP zUS4{3R!&|zj)ak^^!EqbYirvVcvd!7FR9cH9mz{i&$FbbTQ`JyHDPqZTcg#Rt@X<2 zd?Y6WB_mllMQE7&Dak+iHeqB39T^D^PPi=zgBm&|LE+@nh4_l#KN}UuBiD!UlQIpN zDH{!sDD`-ecYwl6vHBVm%#1~`xTvg@70*4pG9^P7hXnjcASdz9VPO+yt3E5qKXmuq zLvxd|qA`bqyV)cdq0>0PpZSUhUaFVX5Utt5m%m>H>x1d<#u>p!r@a$Hi6!}3xLbHQ z*j+0OX4D!CHJQPEj)=8U;-VTH%i~6l1%-dcOkX)oyiWm<-Ma@r!S@xAph1$H#Wf>X zE4h@#pNCBKG@gVUCCsBouvRZe&Ib|l3YYB(OSFQ@N8*5p%vQ6B&S6rrGbg7kcQ0>j zTIwq9t!kNz<0z#{xDBpbJ?!)H1YHnuZ+Ia9+wY#|hI)o=TRa2+ztV zAa8}U9X zqHbBhc{5ZmAqMnffZPS^4?ndE{ex*+N*`Wrl5as`*(q;{^G*bS16c{@Q+H6&gbcSjxy{=0}K=4}rF1G3T?gjw^fsB^N0zWRVds#YgIqe>G$?3|TeR@|#6PF&NL2 zS`*phN_O56`QqFutZdAqB<)o`#o$S-Ze-V_>~DnY6ip^5I}d3;jKGlD2u@?QyF3*@ zWU|+d@EB9=EaR7n7E9t6$+Nz6I2_64ajjM9B|?2cLGY=P^s2fd{QQ8K+uJdQ$(X^d z+79yWF)fcef9)X43x0Y`{6yO!o`qHBc9gQ=eANnJm`6u=%|8)4+v6<0iHA4S}TY^$cSxxW9Bnd)N%)++aUAt%f;qk|J zHTSOU+I&IJ+FoI?u_!*i$oL}b6(6{;7H7-#UViyc?!D$3vS2vJMuk$eF+-CIZaas3 zLec_Y_f+zAAT%>W(hw-j?xr{$E*;6|vC{#2iiHO%xqKeG)%ldW!CP9{)L3!NE^}6v zdDq3s>1oNAFjHfnu31}>pJpr0OzSVN?rKP@>(Mty6;>;FDJUpm z(}c3ZnH4o@Mx(jZY&50?m)4Z0WTYl1rfB1IE}PTib{LY4h8f91Ty#Z2c{R|6e$lDd zAK<*^DsXph)lpVp%GHC3A=_zS;F9?(xAaP7jb&P8pFuA0Pz z1^&R`#%06Sf})}VybiYS>}T@>m$cOm&lbPvS<>HrcGujJ!t}NUfwR|(zZl%w%}UCP zw>e779A_89d?0Aj#{MO&18jxFM`T-uTMy9*AW1@k>Rfwie3AJIEFWtjnOcC~qy1}F z_QmF;XF7V^-g%|{9*3=TM#AAGgV$WQdR1}Wyn;-Zv0_Jdskb(EU4QkwRZ)f3hRo8A z!mKVG03M%mKqQLn4C!TCLBMH zF)^mVib{_ZNc;7eammINRi!RhwcE9{q_v>7!djNLc9E-ht$sz6%kC=iv}9Wy1(uR%bNoPE z%`BvVwlukWmaql746oH%86B@nugEK?!sZ@2b#53o@DH#aTSNQWLIVVNB})hZF5pH@ z3`z>E&@x%cBPd&^V1;6i*;^YMx6YokrLl3#9ACiY3i$YYQ`M|lRaLD`mUPPVlx}Hi z9mcWXdpb7xC^Gc4C!f|58$c!H}z6+c!0C2#{@OqTgI8T>B;Wc zb?akY$>T|On6OW*A|W4B(!Si&iVMH{BLv`0om<% z9N<`hOOAw(@E!8JLJFlQGvq`L0MZM^OR?7q@2ElXJYe-e>OG2(GLi9h?7sT~_uV%v zoa6uSLx1r5=oI8mI+>fei)VIbuo9!Lt?C!P&^V=7+Y)W4`cX484@d7KTUR-3GnVcD4WGaQ2 z9)=LQIM1G{f)bFL04awR&>r_gEuc8fxBbUyrosLL3GoBi`(~1b9@JpOX)7gKG6h++ z$mEVgkPI|h^4vmKXuQIjQ}&8&(TRp=Au38}Td_rFFz7)P0=rQ@op)w}$&?VEl7bxA zwWijttdz={3M2a!(qKPhZtcu8RaSdUVh3~7lzv!R!{59~DaVsiQj(6RB%xwWKfR;M+c|xDr?++>*_GJ3 zB(CY4IUn{QgW%b{=ec73Vsbc4!B0(&y7H1XJDGj_$-}zMJBp&o2FQ6 zf8PA|)GO&q$ONDG&_{k!7DQ-Jp^^=#Fo0$4;K9McgP_x7%|hf)`yQm6* zq}Np)NNk-$?(8Xd^*HNYso71g@}}(6@&=qm z*DL-jyP(W5BtF1mGYiU{Ll^^sqk+Rw00{yxWWqRLDO(=%1AtyP!) z>WrQ<#eZLxmXn=!`Bb3Qm6lXoeAQJCWtdVRIOD7Y_8Pe72V9nA44M1rWR%$9TW)2q zk%8`~KSi0t;5DCV4pA8|F$v4zz!CA4LvK>{&mr8qQMzYCBn+|)-YC9u26(_Gja*$3!A)4hB z0ZEn1iB>D)aW`DoGCB8112%uYv%cOrpZ!vNjTL?L{PRSq#oey*?&9jY^76XiEj6sS zW?|_p#~x#gPDlqj)!{1?{DjQ@ao3# zK(B0GeDK-0)$dAD1Z94fS^%G$6l#nEa&_`490~gFo#(GwvVKDKzCSMNZl`HWSi#9( zCb@q2A1AAyI435nG7KI{=xfqr#)6NI?~$Roui0s{l{jtL3m-kct#R(W@vSfEs zO-sx6RQuZexwU1kCZ(TwG8~S{;V^n=^^t^aD;IS6nlu5edv7sk7FcbLX05-msmqZ+ z|DN$MtgK&>kZZ`c7Uef|HCI_{lAXXsSQi$szGqM*741bPp~6Da#i{h)BJr4zDju7j z5r3uly({C>r%$2&Rw8)+8gpsNM<1n>Za#_LD`+afMTlajkrz!({TQW4dB+P`hu9cf zF3*FrmU!c)MMX`I^8^~B;uf~_>-16VxAJ7bsXM(Fq+}Qj>8a<753FciRq*_?X`Z%< z72u%ZHm&2@q?E#nkVOKYtWAcS{LYS^=J*)BE_Qn3oEppvpJrGnw@GIpPQ%O`*H6io6l&v}o7t9|RTVNL^2~ffdT1E#46L)@ z{^RgB7tN|<`aujjF^Gn z5Ev|7;%|62Bsz7vdMEL@#LVe=#*)6PyRQ!8pi)|toIS2APfF#q^!W0ortC1Bd!`t6c>gpd?mGed1y`H6>p=pq~$D0khU8YpufEgKU4}7WgQtN7!37B=vn)F~vk4+f zuDv?JJ;&kf37$9wQeg1_PC~N~9EMgb$lw?+ypt)^+5BL0^Mg#o=OEVEOD`24xaJz< zIhH1&PGw7^Y?M}#du-Y;)6fAUkQr$F)pKD&L;x%L5AWiHwCUiaiz~#eUCgmde0xe} z+Idq-sTu6_CMW%f@0^sQ&P5;fu#rbmOgWNIo5|^I*Ux?94e>(OclX_F$*Zr5cet40 zjyq5W9wVNkIU4&UD4GCL4Z_V3L5B>@z>X2)%ab;H%=i!anvWw_^P`U;Mk%giCMX8xs8Aac{5%sH1xb6!6b=G1xvjWT=Pyex z^;H%Nt>}{@_~T8R*oR{J&Gy*XuUq9xy>y!m(tGGDulFBRjPC_;!GifZY-8ypWiiYj^pn(7GOj`=M(z)A z9X-nuUt;C6Eg6^7+!4QYu=f3T{?^5^Sk@g!`B&#;SQuXm#Bz zZ`Q(Q7^2FX7fuTI#(C{eKIw3|98W&kJ`ae&Zq~1nCE-=cx}t;M3WEhv+?)~(vohH& z$qoeCg-GbFOo5PSO2%31GVhVb6gIK5`^#NLCU;_@J~=I`VwumktRgc#S)XWdnTpHH z`_FDku~<^fxw&y=i?%d3Z7ELC8xqURnca@M6_u4M>Kxsf=CVYCKBZ`DQ}dQZWgjDr z`-POuUA#06C_vU04&^4k@_;W%a7rhHaCjsbC1WdKR@qvC9|=B!g5ac2-#~TsfX}z8 zre>8-4H>mf5Yp$0+QGWI!CL9P&6;+Q;K5r6XA(50gyp2Ib9_wMD)F>qV<*^ZVIA$} zBkOtCd&nFzg%DM4ke(riIfmNQG9^NGk|v`Lv})=OmVq)AbaoO(myv5Yf(q zW85c6(LRuFb$NCzc(vk|L`U@6=#b@t^KYyzlF*M1$~|QctM!%4j90Drc{L$E^OZbL zQi8#dkQ8q)M0*_WdQU?`rp1zJnO4NKR%?D9n^s&b9y8<{tX45t^sq5LAt@;#-nh{t zR5@KWuH39l&>m@vLW%G;G+0vXNT0Q@*ZamG^^%jXji-^ ze9a&%SCW6{TlGY?$b!~GPLtE=LD7?B`P=awviW`Yl%Su-dK*eh>bKpOWlA9vIj4)_ z{}NrOw1LxsGgs0SPDvJu4k5!5%vY>pRl~KHTB zz-d*kRM;&o&_TTt>;kR{8uwJ zuX3$|&c-yIkT9l0N`%bHOj<7bkWmya8`APgD8fldg7D7@=d9Yg_JB||(2V1Ot7rXI zJSeh@FX^lERMh*$U-U7dt+uue{?&tH(d_oI7qOq` zJlfBL5#2Rb!d@7=pLBbY7kO~RgbihE%OqRKCfze88WFSkjrLNkb9&06ip(#3kBbqp zEeKuq*dS@aY|2Ooh1@I$xo{g%SeHZm0ymF{FT;x)$agL+5qiW`%x=t$iM1HSnAx44 z=3@Jn_Ichlt!+cqfu6?tuJWQYXZ2Pr*Is%k*fXNNIW0|me(-4ErZ~iQ$HlU^q6SxC z^|ajN&2#FPyIm`q>Snc9Ia{XZB(H0$=`Yu4MLnWj7~uA3s)-6AMcQp1TPWC?OZX(< z%b;=JOi0C4x&hCfq+9$j=ZLbbJVF%RYB%&G~)SH1`_C!roREOQ?i8PT49=#WNxVg&ep2XHq)f^0u6 zxFg45+(0Nj5MP|E2XlIr9!waMg4tHNKhIj!tq)sB;(|rE{97;Om$n zbmkV#kTP0LEV2YN*({NwVL4q{V0MuROlC`=U3BV6B|bW9&>Nad2M&1Zn<@mCc;Ky_ zJ29Dr-Tb2t^KI3R1sI z9Xr2HRYVhg;*xQFT8MLK{wMo|Z7iqN`_X~Z>iwWMqW40j_%n9F{{_s*$eRLf8g1~; zz*`JxK?{8Yo+&S+bRLyPu`W_@R0PjdQG=7cG5Sk&XC=0&?F?Qd@y$WMfAHkIQzHal z9oxra7Ep7xI1 z%R!&S4Eq zR$l0k(jl($5ylqNi6a5slBgKnqjZYrA+26}{0^(oGp38veN5+NeijuIH3qgPK8C21 zClEP87IWm&gUk&(<%@4sD#aplNux(T)qpf?Dyrfnk4f@$F4Lx;ag^7Z>~OU7p)LrT$XS?G@d2o@YXW zU=|?YUIwyG!@+_5CvJm}*~LGxi-YH&676*V=zKZLiTFNX&cYJ~xI#{T!LfKjqKFldpoeBt6A&^b|!kkRUp`J%9$aLO_@oyoWuJVvmcn zr-&_pVDt&;g*@8g4eMD#f+j? zIfpEks6vJ-0;bm}ZfsJ3CIAzr!!R~EBr+{3^vFTb2qAi$PVt*IqDVTj76-z@OX8Jq zVz+=SqfTrjn%sCor>iv@({;K`Q{wSwK%7@7hr*_sGoy4lY2+dVeg2%SN1t>&#5`>RQ>5Z_+DQ3%vlW^~V8J>Qo*+ZLfg#20wd3OoHn{pFRVj_E!}d82RF&X}Qv z<@I_N9b?|q>0cjn>7`Mr$q`~|OmedD?|AcYSKp@Cov+0B${fw{QC%!vUs+ty9t)0; zt69O`5j^Bk$2qjHBud_1a;%X}QT+3hPxiCx`|&OguQ=>Kj30Ufml(s7cjHNDVd^Kz zSFAiuHH;pTYEUW(*CW-0x&+9~PU!yY;0;U-*G9EKpOFR_=i8dZ-+l2#{}*4dCRQo$ zi+!Hm>pbQ^hQDO4hG0+Z4+BRBHBn-bW)NrJmSHZ<0E9Wvd@=dMo;GR64g?2o)79ag zpZ4ssgnWl1U-c4S@hC8msr52Z3 zO4U+oEhzHNWIioVl( z*w{wB+{f_uzBj3Zdqu^;~732dy>g($Eu@0GRK^9%F(yX`c+L&P6#o^lGD%#^GAnl{K+wCy|Ur&p+5WndBFKWK%wXDW(HeutG+k01Tliy0C zJ)#MaA_Fh@Iz*mX3?qkCPr{RlCdBHrn?Bon;tQkfdi2%eeBz@IYx!g4raPx3U^^6^ zqYq0WxoMBB+HL;C^f;cy(Q`GyysS_AI~aX^(K?LPVs-sEGaDQx8y2g16$|U(!b~QY zz?KNSSYKu_@kWJ4|IoJ+W7y+(-8#iIA>qo*u>PfSt9>Xazfql(kShug!Er`S=|;YT0-^wU+V%*}d8TC(cr z=9ZX7EWu&rV@ctlaj6#kPcX{J8+5*bU2OKk2QjnZWA^ECMJsq^JIHd;0wd&c=Eweu z!gjOIk0rejT@4TlCg`IP#6=$B?xG-1R#%)48!=qWt-8sqGHZ_>JmnN~$kAV|auZVr z!pG?K^&M-$`Vt0CA?ahV+qxwnak>8v>HW9XVWBrdWASwd%EEv+Jo={g9oYrPu7bn( z*eI!`fNITn{wXx)PM6O0&wK1LotM9VB#XycPTrY3(WCQX#q@djf({l(tcDsfTpmDA zE?m6$$bBzDI!!N;j=tW9u-Obc2fS5Pp743`HdgX#x9M1?m&S6w8Qq7EwB4GT<7jGM-_ci~!9SN8oO85C z$dygRK}*HhbuS0|F7a7$0Qwk4zAv8#>%HGlxAE z?Zf!8(fr+!+J2T_wZ!mjZsw!Y;ZXS6nf=UvUtjM*w0i+guODaT^x~-$Jumzjdfw3; zMmZ!Q@kf^c4-NhoG?<0OF;VD&o8WO(*&f$$cw1N#lZx);0oR}Q{*umLyf|8^yxE1M z1?Q+7d!muHM-L_r=jJdd5j(av<{?WzRP4buqCcG;xW)rW2`BbA4quDx+%0(C+d*CDIh# z^dg&PhQ8T6?#yK)>zc<^kK2r&0|z&c+^nn>#ud1b@4i;Pqx?1esS6pp18kr-Plf}i zeeYuZ3?O}Px8&cEu6J}L1f|~)j7%j(w)6dlQ+pT$21_s*mO#-I*6G`*@_z39R#nEb z{$s1qjvF}g?7Q}evuyj1du9P8h!e@_kf z8119azrW~xjF%XL^f)3s1~CfZ;YJQ8<6vJsa%5O-9$mcnC||vePd)PNNmANZU)}rA z)mOiI$*FBu9F%aR`bh_oP5&B{P#yLUaj)h&Y4?#wLzgF$_G|swJcjd z^bQ|40{);!D#2U=jllVB@Pvo< z3h((PbLY|OKSaf7|H&t+_sql|cJLFT;96_NTe3xCwYS^UMP{`*<><|)l$k9@Ut2AS zef+Vjfn($_YJLe$pHQ6QSyFgWIlaGB96u)dlHhm<$`fa_%~$efnqjQ`3Fcwr$dmDN zU-6ZsSU!lIOxUQAN%*<0vXY;CqIsI$IyNveCvVuWyquB2F=N7`bMw=N<>!tLjTsXh ziA^hpXDmGR#4*7U*+`O~H##(SObAJ2^1xV?RaQ&L#;z2ZJ~M0FxU7M{oa{n;lRXeC z?$J+#uPAF-rfZ00rT=BEJj$E=l*a#$l^Bwnl`|@1r{e!F!}9ZoxtYrq?4IVvq-12J zTOUg6Cj6ZFcym!g2Ti6+rDlk@AS-tHB-j~Ao^j$aj;o5cRQgNA#@g4XghKyQd&1Ds=mhw5pWulz z1#e=C#WSNePhz9R!il|ah>a76hso0bM3-YL#2OHwFi@=4{R z1z4EJYe!&cXT1mYo*TJFjd8}e(Z^N@VqLsk<|-!L)ROUUc>#$93H@F`E(=rz$4|)0 zD_mGuJ#o^sDHB4YD{3eE&mgV;d7q9WqBY0lOq*7C$`}F8EibPxiO%~UckdwZlGJz% z`&3*!#q4iZW9zp2_lgrK!(`CoJ2OV*WaSPKW;Hk#s+}k7Cpz)isWl5T)6?>@h2@oI zH4RC^V^mLwvkqbm8ufuSJS!u`R!MQKnOazwo#Vy`(yxgNM{U&ea47D>`YtI?T z_1=Ppc;FB-(e`y3c^ThpN^;l7sP>}>rUYMNSG=#}u6SaGUG8uZ@~9bg_#XW>ow$B+ zT3X4Jsnv6;=SId)88c$Um^k|P=&m&XRJFat)HHi{39K>4GF|K{t#%=owH*peOYoFD z4p;F_3>>@YEz|BLuirb3%q#!8y!R1ts%tHve+U4=HtvNS*U~>?p3}fdVeDS9&-)!> zCp*m8ywzb5x2VK9XDy9lu=!-k(2xZ&F1t=35N z22jeDlnnQqhUi|rw;h3>vp*YZ8=XO~?8{-PqP7H+xX~v%>s#rS*IzH+Eg5ko`|?dWgOle6U2@XDGs)t-5LD~fk*}-1ShDC?R#3bp23h}bMk@=I(Ekn zP|pX2Yd`_p>>$5*2OiD8ZvOSl=hceLvu6tq+-bm_DB>3)MZ7B*Hjo_qI+u=or*o90;evbPzQYGFs@u|uNOU=F72DerYl*yWhC|!QD3H~rAPM` zRa6v-pZG!vKf3gkQ0Nr#i;T$0896d1X9PG5(`!+Bg3x&Ot!ocIJkHWXdTnf*`WPVT z-QCv(tr6{kUWi^?M(@V9&m_7I@+x7aVz!qQgHQFw9G@t7qT7N=1+`6MPe?7KSDrF7 zF|B;usDkt`p5+fAPfM(oun7alFZ$S-ooAB~2xKb>ISbmSvUX7_^NcF~P3=XOvGz|f zciNXhKIq%5_A6s@mAiV9IFOY(WaiWp$Oq*^QfJmq41>lEeG~h#F@q-F;ktA-rgwji zZ3l=J=(GA}VGXC$c6aLIwm@;th=Sa#5hIQiPo7*XexdC2tc>j8=@2@JuINkYo6`3a z!8;cUvSLY7)1jMgLQHpGq^|<&e8n4T$vfH>Dbzkw(eCQ>?Bd|4(qUO8V=IbYDjuGg znziI4mNq(TWI)iZL+@3my4kw+TsHtfBY-bzMRW0%g}m-^oCdxy@JyuwNBeTh8sy=YCI*s>TUruW^9 zz3gtaW25Qhq*J0#w=#Mx3OjWXy@tFcc4&<4WB&m#of6X|EDX|Yp;r>o7d+M~jX40! z!xFPvV{V~W(iDkl)|j|- zkIJ~?+D4H~*v4o+(p`gegDs#)qAj2+O@9lBG`0m~V_<`Jey0UwWBOabC@kRnN}_H_ z9Jl|)EMWAw7Lc8Q(jWZaEg(A~X82+Nq!MN|pm&SidxQmKHOd0Ag1!*jdt3`RIjLY~ z?Fmz{5m3P?X$fiN<45LXN(%`2wZz(DkOh=&A=*0ZlA#)I^~&gIcu@_;k-+#uKN}unDk|JAuI-=F)B+0Onzd!O02| za|@dQD=E_4tTAhm<~@nIWf108jk%tUfdv$~exNZsXcSiRePC{jr8xm<1cyJ2VQPT6 z1(;0&10VD?Sme#Hw$QDiur9Bys#lH|G2Ww7;mHbbSjN0pa&8%&x_LA1)%YO3T9E#? z@X5qVPuq8NYYAbqYZuKM;7l@d`Q<&fn%Gy-GyZ4)Y65cL4HQ}v^W^<~GO^WsbnmQl ztHm&0ea`irOMB@0nyLBY#^q0~>ACDOSo`nFEv&GUyQybs=karILD~3NZeZEcyAv~4 z;uBT)O%ACGC;O5Ti_0g5eBMzmk|!l3#wSi2SCE()9~2`|7=BV0+YIX1S8o9I!QP*m zbro)Cd3eIaNckcwe@sStD$XtnPbe5O94{QOR{u)WzOWFr|Jl&Yp~=aaX@!0OdEzQc zETC5!netl{?2#ZmX!tn7SAMdyz9)LgwF|CmsGUm>Uw?h4957`1z87NYAupZ&w#(`- zik^A=^slll@N!^PnD8t0QcNi!o%zAZzSQJk@suL3Z&dUuo5)E?aS4gj3P;5cOAM;w z6~7qMHe-YrtB$@+CYtUigXIz3ugJ4WgT(POH73&N2`r)Sd}(bV#cZ(T-p@G>Zn*d+PTGImgsL?xx@8K zm#_poJ1F&|fH|~&{audZ-hv_*UDjUW^?Ije4o}R9FRZuei&l5| zn>d)i;=xkYvp4;?j^t0s%N9pw1;!VQ&&?8Q2eQ+q73Ab#+V1+Kp-Cx89?#gx08$eL zAX*RJTe6gFe{I{b^sgd)vRKV3watEt z5tATFvv>Ht$rTF=#(KS@ZCzvSCwSvirjE=>9PS-i@1nHv5zJ zeZ6aaSHqmSY|~(2T;JMy+G&{6SRDNv`=Y-He(_oYr>5iUKz#qLb_@|V1WyEVv(Ckn z#@v*d+q!LAYhl5inK?Z@IFR9@NoQp@51Bl4+qP{x#>~v<92(Az-bmNcb9;}1a#LRg zcG6pCWC%-zxBlW&yiia~iQ=82INCs-pddB^4Gp{ZoJ$ji@i^l3&OCd4!f-2&$6sCh z&Xf2u|E-);hx^O3agJp=83QZ2p<^`ahyTg8q8asIiX$lNJ$0{--@A!CL49y6qPx470~TY`r& zM|~7JeZr(!!H=+)!|cpamu>bGjw}oW5O|31L-E`O&uE`GHwSHILYDZZj>H9nSk$_g z7xp$|)H8G}$Ik8I00BbYkB)9fkcWty>21BuNT$=RS5$m7luBpg7^B%iq`8&edSsuS zLX-naNFfR3k@wr9qd`n2>s{5`iZ)o-7s37;V`2T!x`Uqb#PMiYECl(J3fJ>q^x3C> zMSQ>M_t^K+JaXB*=%jh@Ko<5*6g07T6hk@2^d7wTR(}&|oZwzEn5Hg(32acB##30~ zaT*po*q^Ln6VC{zX_!NLb2Mz>xoN(JJ*e$D8unt1`!Wstkhe+0aab+Zrr~(dS*zg$ z?D%j2w1t@1`N4SH5MG3UiIGFfG)#>oa<+zXWMc_WsQ#`84ZV_;ix4dd;wU3cO`-f0?-*#Tao z;dszlq~Qc(2w$qXIDq}@~)<~*2?C#&W8GasEUrZ z)eWr$4d->&HFp*?H?6Ge>h5UhoL46jF01P(SaD`U)5^xK1&vLuRXtrDb+g2s4CXbg z>~5~>IK831sk>!G#Xt-0qlcc{tAo*z%&3e z17R25YIYcPNY{$5{0i^|V;i70K()ZP8LNWq4n*dpt=8n`xN z1;W8)6(H9Xq@RKmQ;ri~i?(QjteVh9?VvCllIxJt6S!8aL@F?5$~twT+&W;oB)$N$ z6f$lAW;Np13~K4Muq|s*<|?G=LTfu3WVgG}F;%N-S%T2O`G8g+{aRTwp@Axe;AXij ztrKBA@ZBg`$e;tT&?u31K5XPHT<4%Jg0lkbf-TxZP@dI~f~eQPHVc4T@l)D=ymD=B z>QGmu|Mk% z07n}y5GhHY5>FCHqOpu58NVdSB!#4c_5;R7G6d^y#~QsxAHFyl zO42Z%e~i_g>3Cvnz)@})#%_{HvPd?`A-TqjB#-19FOdR*Z&Ao7G8$gm1Tw}rjAhm1 zNFf<-Oe7P?MD)X#$t2?y<5lAbnQZ)lOd;6Wi3G5!4i=7tj1x#$e5^!@jrU>g0;GhL zlBwiGNH}QhA=8XMl9R~EOGVn)bpGKbU}MP#n=cjG9TN9L0SWFg*xok7kdi^y3xQ)4e#Z2TFgDSyv+ot#b1 zA?K1MWGSg5%gAz4Pa4PyvJz`Ir;_N#BNt%jl_qi_xrkg$E+Om5rQ|YWHMyKzL9QfMk*kenvcXtIt|8Zw z>(G)d#%6LoG^g5#62oXCH;@~#lfWnB`|t*8jCQh-++=i+n+Y}oBtIax8J*;ZWRr0o z-q+nh?j(1SAHj0WAa@(zkRM}5#h;KZ#!T$%e-AY94Wo&@*-c&`FOrwY%f?U0D>za1L1_0u^ofP!Rq_Y2 z$2f!h(fGhPMqVR-GOi(ejWfxgvD^0>WS_CfxESAlzGLF^CyJ>zWSGjhmyjQopyPX3KGAzvEj zkgtr7$k*gB`Gy=JN69hLOQNI?rydbXu}amX94F`EEDd})5{DB%a8MjgqRBLcreej` zP@0CFsMGPrHiKr;ESin?rMWbZ=3|BU2s)CEqNC{;c*o;tAstUAz*C+?C(|i7ea24% zG)O}@r6)p*XfZ9p$vRW%iF6u0iJpwzrpk;T8F$liT0tx6bXtWydQYV_*vVuDok?fW z)3JZ#99m1~(s|gAWC2}B&!A^wkH)j`Rpj^R+4y$)T)KoVrFHnqd^xSB4Ri(eE^MSt zbQN8VeeqgoD{Z6g^gP-@J82i~rfcY0+C$IB^V$XULV6Lsm|jBH(@W`P^m2Ly-l1GY zucjO5HS}6~9lf63KySoOpd0B;cu=_o41;U5V3{n7WwRWX%ko%0D_|qoNNhGUnvG#&**I3n#=L$~UCJ(F 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 Z%h#_b*~^!E-WSq^XhsVQb2wuQ{{qd0QA+>- literal 0 HcmV?d00001 diff --git a/client/ui/frontend/src/assets/img/tray-darwin.png b/client/ui/frontend/src/assets/img/tray-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..75df803d82744a9b328bb5ff1985bec49932c968 GIT binary patch literal 196162 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%0D5D3-WVk1Ic=&A5S41dfB5!h;xWA?-O%o2h% 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