From 5411fa4350c98a838f9d2fd49bb242cb276be63c Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Fri, 15 May 2026 12:56:09 +0200 Subject: [PATCH] remove old code, add german lang --- client/ui/CLAUDE.md | 4 +- client/ui/frontend/CLAUDE.md | 14 +- .../netbird/client/ui/services/i18n.ts | 46 ++-- .../netbird/client/ui/services/index.ts | 2 - .../netbird/client/ui/services/models.ts | 61 ----- .../netbird/client/ui/services/preferences.ts | 33 ++- .../wailsapp/wails/v3/internal/eventcreate.ts | 5 +- .../wailsapp/wails/v3/internal/eventdata.d.ts | 5 +- .../src/components/NetBirdConnectToggle.tsx | 136 ----------- client/ui/frontend/src/globals.css | 10 +- .../ui/frontend/src/i18n/locales/_index.json | 1 + .../frontend/src/i18n/locales/de/common.json | 34 +++ client/ui/frontend/src/layouts/AppLayout.tsx | 27 ++- .../frontend/src/layouts/ConnectionStatus.tsx | 117 ---------- .../src/layouts/ConnectionStatusSwitch.tsx | 8 +- client/ui/frontend/src/layouts/Header.tsx | 27 ++- client/ui/frontend/src/layouts/Main.tsx | 12 +- client/ui/frontend/src/layouts/Navigation.tsx | 61 +++-- .../frontend/src/layouts/SettingsLayout.tsx | 36 ++- .../modules/appearance/AppearanceContext.tsx | 86 ------- .../src/modules/settings/Settings.tsx | 4 - .../modules/settings/SettingsAppearance.tsx | 49 ---- .../settings/SettingsNavigationTriggers.tsx | 6 - client/ui/frontend/src/screens/Status.tsx | 211 ------------------ client/ui/main.go | 7 +- 25 files changed, 175 insertions(+), 827 deletions(-) delete mode 100644 client/ui/frontend/src/components/NetBirdConnectToggle.tsx create mode 100644 client/ui/frontend/src/i18n/locales/de/common.json delete mode 100644 client/ui/frontend/src/layouts/ConnectionStatus.tsx delete mode 100644 client/ui/frontend/src/modules/appearance/AppearanceContext.tsx delete mode 100644 client/ui/frontend/src/modules/settings/SettingsAppearance.tsx delete mode 100644 client/ui/frontend/src/screens/Status.tsx diff --git a/client/ui/CLAUDE.md b/client/ui/CLAUDE.md index 4ba67b015..442506e6d 100644 --- a/client/ui/CLAUDE.md +++ b/client/ui/CLAUDE.md @@ -20,9 +20,9 @@ Each service is registered via `app.RegisterService(application.NewService(svc)) ### Frontend (`frontend/src/`) - `app.tsx` — top-level routes. Hash router with `/quick`, `/browser-login`, `/update`, `/session-expired`, `/settings` (own layout), and a root `AppLayout` that hosts `Main` and a `*` catch-all. -- `layouts/AppLayout.tsx` — composition shell. Wraps `Header + Outlet` in `AppearanceProvider → ProfileProvider → DebugBundleProvider → ClientVersionProvider`. +- `layouts/AppLayout.tsx` — composition shell. Wraps `Header + Outlet` in `ProfileProvider → DebugBundleProvider → ClientVersionProvider`. The wide-panel `expanded` state lives here as plain `useState` (no persistence) and is passed to `Header` via props and `Main` via Outlet context. - `layouts/SettingsLayout.tsx` — used when the settings window opens (route `/settings`). -- `modules/*/Context.tsx` — context providers (`appearance`, `auto-update`, `debug-bundle`, `profile`). +- `modules/*/Context.tsx` — context providers (`auto-update`, `debug-bundle`, `profile`). - `pages/` — full-screen, single-purpose pages opened in popups or via top-level routes (`BrowserLogin`, `SessionExpired`, `Update`, `Debug`). - `screens/` — content shown inside `AppLayout` (`Status`, `Peers`, `Networks`, `Profiles`, `Settings`, `Update`, `QuickActions`, `Debug`). diff --git a/client/ui/frontend/CLAUDE.md b/client/ui/frontend/CLAUDE.md index 041d95c4b..19475fbf0 100644 --- a/client/ui/frontend/CLAUDE.md +++ b/client/ui/frontend/CLAUDE.md @@ -58,7 +58,7 @@ Bindings are regenerated from Go via `wails3 generate bindings -clean=true -ts` | `/settings` | `Settings` | `SettingsLayout` | Auxiliary window (Go `WindowManager.OpenSettings`) | | `*` | `` | `AppLayout` | Catch-all | -`AppLayout` wraps `Header + ` in this provider order: `AppearanceProvider → ProfileProvider → DebugBundleProvider → ClientVersionProvider`. The order matters — `DebugBundleProvider` reads `useProfile`, and `ClientVersionProvider` paints the `` so it has to be outermost in terms of z-index but innermost in the tree. +`AppLayout` wraps `Header + ` in this provider order: `ProfileProvider → DebugBundleProvider → ClientVersionProvider`. The order matters — `DebugBundleProvider` reads `useProfile`, and `ClientVersionProvider` paints the `` so it has to be outermost in terms of z-index but innermost in the tree. `AppLayout` also owns the wide/narrow `expanded` state as plain `useState` (no persistence) and passes it to `Header` via props and to `Main` via Outlet context (`MainOutletContext`). `SettingsLayout` uses the same provider stack minus the `Header`. It also reserves a 38px `wails-draggable` strip at the top so the macOS traffic-light buttons (the window uses `MacTitleBarHiddenInset`) don't overlap content. @@ -77,12 +77,11 @@ layouts/ # AppLayout, SettingsLayout, Header, Main, MainRightSide, lib/ # cn (tailwind merge), color (hash → hex), welcome (console art), # MainModuleContext (unused legacy) modules/ # feature folders that own their own contexts/state - appearance/ # AppearanceContext (localStorage) auto-update/ # ClientVersionContext + overlays/banners/badges debug-bundle/ # useDebugBundle hook + Provider wrapper peers/ # Peers UI (currently mockPeers; not wired to daemon data) profile/ # ProfileContext - settings/ # Settings root + per-tab files + SettingsContext + accent egg + settings/ # Settings root + per-tab files + SettingsContext skeletons/ # SkeletonSettings pages/ # full-screen single-purpose pages routed via app.tsx BrowserLogin.tsx # auxiliary window content @@ -90,7 +89,6 @@ pages/ # full-screen single-purpose pages routed via app.tsx Update.tsx # enforced-update install screen (real one) Debug.tsx # legacy debug bundle UI, superseded by SettingsTroubleshooting screens/ # in-window screens (mostly legacy; pre-AppLayout era) - Status.tsx # legacy detailed status page (not in current route table) Peers.tsx # legacy peer-detail UI (uses real Peers.Get data) Networks.tsx # legacy networks UI Profiles.tsx # uses ProfileSwitcher.SwitchActive (current preferred path) @@ -165,9 +163,11 @@ While `config` is `null` the provider renders `` instead of c **PSK mask quirk:** The daemon returns existing pre-shared keys as `"**********"` in `GetConfig`. Sending the mask back round-trips it into the saved config and `wgtypes.ParseKey` fails on the next connect. `save` drops the field when it equals `"**********"` so an unrelated toggle save doesn't corrupt the stored PSK. -### `AppearanceContext` (modules/appearance/AppearanceContext.tsx) +### Wide/narrow panel state -Pure-frontend UI preferences persisted to `localStorage` under `netbird:appearance`. Fields: `connectionLayout` (`"default" | "switch"`), `expanded` (bool — drives the wide / narrow window mode), `showPeersNav`, `showResourcesNav`, `showExitNodeNav`, `showProfileSelector`, `showSettingsButton`. `Header.tsx` writes `expanded` and resizes the OS window to match (`Window.SetSize(925|380, 615)`). +There is no appearance context or localStorage. The `expanded` flag lives in `AppLayout` as plain `useState(false)` and is the only shell-layout knob. `Header.tsx` reads it via props (sets the panel-toggle icon and calls `Window.SetSize(925|380, 615)` on change); `Main.tsx` reads it via Outlet context (`MainOutletContext`) to decide whether to mount the right-side panel. Every app launch starts small — no cross-machine drift. + +Nav-item visibility (Peers / Resources / Exit Node) and the header buttons (profile selector, settings) are hardcoded to always-render in `Navigation.tsx` and `Header.tsx` respectively; the previous toggles are gone along with the Appearance settings tab. ### `DebugBundleProvider` + `useDebugBundle` (modules/debug-bundle/) @@ -278,8 +278,6 @@ Fonts: Inter Variable (sans) + JetBrains Mono Variable (mono) — both shipped u - **`screens/QuickActions.tsx`** is wired to `/quick` in the route table but nothing on the Go side currently navigates there. - **`UpdateAvailableBanner`** is force-enabled via `FORCE_UPDATE_AVAILABLE = true` and additionally TODO-commented for the "only when management has auto updates enabled + force updates is disabled" case. - **`lib/MainModuleContext.tsx`** is exported but unused. Candidate for deletion. -- **`ConnectionStatus.tsx`** (the non-Switch variant of the main toggle) is local-state-only — it does not call `Connection.Up/Down` and shows hardcoded `peer-hostname.netbird.cloud` / `192.168.0.1`. It's a visual prototype the user can flip to via `connectionLayout` in `AppearanceContext`; **don't rely on it for real connect/disconnect behavior**. The real one is `ConnectionStatusSwitch.tsx`. -- **`SettingsAccent`** is a 10-clicks-on-the-version-label easter egg that renders a falling-`TEAMNETBIRD` canvas overlay for 9 seconds. Kept on purpose. ## Wails Go API reference diff --git a/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/i18n.ts b/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/i18n.ts index ebf67d817..e94e9c8a4 100644 --- a/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/i18n.ts +++ b/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/i18n.ts @@ -2,10 +2,10 @@ // This file is automatically generated. DO NOT EDIT /** - * I18n holds the embedded translation bundles and serves them to both the - * tray (Translate, used on every menu rebuild) and the frontend (Bundle, - * optional Wails-bound endpoint for runtime fetches). The bundles are - * loaded once at construction and never mutated. + * I18n is the Wails-bound facade over i18n.Bundle. It exists only to give + * the binding generator a service type with the context.Context-first + * signatures it expects; the translation logic, locale loading and the + * LanguageCode type all live in client/ui/i18n. * @module */ @@ -15,50 +15,30 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import * as $models from "./models.js"; +import * as i18n$0 from "../i18n/models.js"; /** - * Bundle returns the full key->text map for one language. Bound to React via - * Wails as an optional endpoint — the frontend can either import the JSON - * files directly through Vite or fetch them through this RPC. The returned - * map is a copy. + * Bundle returns the full key->text map for one language, letting the + * React side drive its own translation library (i18next, etc.) off the + * same source bundles the tray uses. */ -export function Bundle(code: string): $CancellablePromise<{ [_ in string]?: string }> { +export function Bundle(code: i18n$0.LanguageCode): $CancellablePromise<{ [_ in string]?: string }> { return $Call.ByID(1780869897, code).then(($result: any) => { return $$createType0($result); }); } /** - * HasLanguage reports whether a bundle is loaded for the given code. - * Preferences.SetLanguage uses this to validate input. + * Languages exposes the list of shipped locales to the frontend so the + * settings page can populate its language picker. */ -export function HasLanguage(code: string): $CancellablePromise { - return $Call.ByID(3348861033, code); -} - -/** - * Languages returns the list of available locales. Bound to React via Wails - * so the settings page can populate its language selector. - */ -export function Languages(): $CancellablePromise<$models.Language[]> { +export function Languages(): $CancellablePromise { return $Call.ByID(768152924).then(($result: any) => { return $$createType2($result); }); } -/** - * Translate resolves key for the given language with a placeholder pass. - * Args must come in {placeholderName, value} pairs (e.g. "version", "1.2.3" - * substitutes "{version}"). Unknown keys fall back to the default language; - * if even that fails, the key itself is returned (debug-friendly — a missed - * key is visible in the UI rather than blank). - */ -export function Translate(lang: string, key: string, ...args: string[]): $CancellablePromise { - return $Call.ByID(2739709937, lang, key, args); -} - // Private type creation functions const $$createType0 = $Create.Map($Create.Any, $Create.Any); -const $$createType1 = $models.Language.createFrom; +const $$createType1 = i18n$0.Language.createFrom; const $$createType2 = $Create.Array($$createType1); diff --git a/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/index.ts b/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/index.ts index beab6e525..aa9a99b02 100644 --- a/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/index.ts +++ b/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/index.ts @@ -36,7 +36,6 @@ export { DebugBundleResult, Features, ForwardingRule, - Language, LocalPeer, LogLevel, LoginParams, @@ -53,7 +52,6 @@ export { SetConfigParams, Status, SystemEvent, - UIPreferences, UpParams, UpdateAvailable, UpdateProgress, diff --git a/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/models.ts b/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/models.ts index ec94816f0..d561338bf 100644 --- a/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/models.ts +++ b/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/models.ts @@ -344,41 +344,6 @@ export class ForwardingRule { } } -/** - * Language describes one shipped UI locale. Code is the BCP-47-ish key the - * frontend and the preferences file use; DisplayName is shown in the language - * picker in its own script (so a Hungarian user sees "Magyar" even when the - * current UI language is English). - */ -export class Language { - "code": string; - "displayName": string; - "englishName": string; - - /** Creates a new Language instance. */ - constructor($$source: Partial = {}) { - if (!("code" in $$source)) { - this["code"] = ""; - } - if (!("displayName" in $$source)) { - this["displayName"] = ""; - } - if (!("englishName" in $$source)) { - this["englishName"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new Language instance from a string or object. - */ - static createFrom($$source: any = {}): Language { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new Language($$parsedSource as Partial); - } -} - /** * LocalPeer mirrors LocalPeerState — what this client looks like on the mesh. */ @@ -1073,32 +1038,6 @@ export class SystemEvent { } } -/** - * UIPreferences is the user-scope UI state mirrored to disk and to the - * frontend. Pointer-free because the whole document is rewritten on every - * change — there are no per-field partial updates. - */ -export class UIPreferences { - "language": string; - - /** Creates a new UIPreferences instance. */ - constructor($$source: Partial = {}) { - if (!("language" in $$source)) { - this["language"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new UIPreferences instance from a string or object. - */ - static createFrom($$source: any = {}): UIPreferences { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new UIPreferences($$parsedSource as Partial); - } -} - /** * UpParams selects the profile the daemon should bring up. */ diff --git a/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/preferences.ts b/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/preferences.ts index 0dc1c9835..658cd32c4 100644 --- a/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/preferences.ts +++ b/client/ui/frontend/bindings/github.com/netbirdio/netbird/client/ui/services/preferences.ts @@ -2,9 +2,10 @@ // This file is automatically generated. DO NOT EDIT /** - * Preferences is the user-scope UI preferences service. Read at app start, - * updated by the React settings page (Wails-bound SetLanguage), and observed - * by the tray which re-renders its menu in the new language. + * Preferences is the Wails-bound facade over preferences.Store. The store + * itself owns persistence and the subscription channel; this type just + * re-exposes Get and SetLanguage with the context.Context-first signature + * the Wails binding generator wants. * @module */ @@ -14,34 +15,26 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import * as $models from "./models.js"; +import * as i18n$0 from "../i18n/models.js"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as preferences$0 from "../preferences/models.js"; /** - * Get returns a copy of the current preferences. Bound to React via Wails. + * Get returns the current user-scope preferences. */ -export function Get(): $CancellablePromise<$models.UIPreferences> { +export function Get(): $CancellablePromise { return $Call.ByID(3500743391).then(($result: any) => { return $$createType0($result); }); } /** - * SetLanguage validates and persists a new language preference, then - * broadcasts the change to the frontend and to internal subscribers (tray). - * Bound to React via Wails. + * SetLanguage validates and persists a new UI language. */ -export function SetLanguage(lang: string): $CancellablePromise { +export function SetLanguage(lang: i18n$0.LanguageCode): $CancellablePromise { return $Call.ByID(3710099805, lang); } -/** - * Subscribe returns a channel that receives every persisted change. The - * unsubscribe function closes the channel and removes it from the list; - * callers must not close the channel themselves. - */ -export function Subscribe(): $CancellablePromise<[any, any]> { - return $Call.ByID(2696446779); -} - // Private type creation functions -const $$createType0 = $models.UIPreferences.createFrom; +const $$createType0 = preferences$0.UIPreferences.createFrom; diff --git a/client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts b/client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts index c4e5ed3fe..83fb78bce 100644 --- a/client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts +++ b/client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts @@ -6,6 +6,9 @@ // @ts-ignore: Unused imports import { Create as $Create } from "@wailsio/runtime"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as preferences$0 from "../../../../netbirdio/netbird/client/ui/preferences/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as services$0 from "../../../../netbirdio/netbird/client/ui/services/models.js"; @@ -22,7 +25,7 @@ function configure() { // Private type creation functions const $$createType0 = services$0.SystemEvent.createFrom; -const $$createType1 = services$0.UIPreferences.createFrom; +const $$createType1 = preferences$0.UIPreferences.createFrom; const $$createType2 = services$0.Status.createFrom; const $$createType3 = services$0.UpdateAvailable.createFrom; const $$createType4 = services$0.UpdateProgress.createFrom; diff --git a/client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts index 7f2eddbc6..c09d0ecb4 100644 --- a/client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts +++ b/client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -5,6 +5,9 @@ // @ts-ignore: Unused imports import type { Events } from "@wailsio/runtime"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import type * as preferences$0 from "../../../../netbirdio/netbird/client/ui/preferences/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import type * as services$0 from "../../../../netbirdio/netbird/client/ui/services/models.js"; @@ -13,7 +16,7 @@ declare module "@wailsio/runtime" { namespace Events { interface CustomEvents { "netbird:event": services$0.SystemEvent; - "netbird:preferences:changed": services$0.UIPreferences; + "netbird:preferences:changed": preferences$0.UIPreferences; "netbird:status": services$0.Status; "netbird:update:available": services$0.UpdateAvailable; "netbird:update:progress": services$0.UpdateProgress; diff --git a/client/ui/frontend/src/components/NetBirdConnectToggle.tsx b/client/ui/frontend/src/components/NetBirdConnectToggle.tsx deleted file mode 100644 index cd2fde6e8..000000000 --- a/client/ui/frontend/src/components/NetBirdConnectToggle.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useEffect, useState } from "react"; -import { motion } from "framer-motion"; -import { cn } from "@/lib/cn"; -import netbirdLogo from "@/assets/logos/netbird.svg"; - -export enum ConnectionState { - Disconnected = "disconnected", - Connecting = "connecting", - Connected = "connected", - Disconnecting = "disconnecting", -} - -type StateProps = { - state: ConnectionState; -}; - -type NetBirdConnectToggleProps = { - state: ConnectionState; - size?: number; - onClick?: () => void; - disabled?: boolean; -}; - -export const NetBirdConnectToggle = ({ state, size = 140, onClick, disabled }: NetBirdConnectToggleProps) => { - const [visualState, setVisualState] = useState(state); - - useEffect(() => { - setVisualState(state); - }, [state]); - - const handleClick = () => { - if (disabled) return; - if (visualState === ConnectionState.Connected) { - setVisualState(ConnectionState.Disconnecting); - } else if (visualState === ConnectionState.Disconnected) { - setVisualState(ConnectionState.Connecting); - } - onClick?.(); - }; - - const padding = size * 0.075; - const borderGap = 2; - const borderInset = padding - borderGap; - const innerSize = size * 0.7; - const logoSize = size * 0.26; - const pingInset = size * 0.075; - - return ( -
- - - - - - - - -
- ); -}; - -const OuterRing = ({ state }: StateProps) => { - const isActive = state === ConnectionState.Connected || state === ConnectionState.Disconnecting; - - return ( -
- ); -}; - -const BorderInnerRing = ({ state, inset }: StateProps & { inset: number }) => ( -
-); - -const InnerRing = ({ children, size }: { children: React.ReactNode; size: number }) => ( -
- {children} -
-); - -const NetBirdLogo = ({ state, logoSize }: StateProps & { logoSize: number }) => { - const isConnecting = state === ConnectionState.Connecting; - - return ( -
- NetBird -
- ); -}; - -const PingRing = ({ state, inset }: StateProps & { inset: number }) => ( - -); diff --git a/client/ui/frontend/src/globals.css b/client/ui/frontend/src/globals.css index ce7bee58c..4cc483f4d 100644 --- a/client/ui/frontend/src/globals.css +++ b/client/ui/frontend/src/globals.css @@ -16,8 +16,16 @@ body, 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/90 font-sans text-nb-gray-200 antialiased; + @apply bg-nb-gray font-sans text-nb-gray-200 antialiased; } .wails-draggable { diff --git a/client/ui/frontend/src/i18n/locales/_index.json b/client/ui/frontend/src/i18n/locales/_index.json index 263e4f491..67dded15b 100644 --- a/client/ui/frontend/src/i18n/locales/_index.json +++ b/client/ui/frontend/src/i18n/locales/_index.json @@ -1,6 +1,7 @@ { "languages": [ {"code": "en", "displayName": "English", "englishName": "English"}, + {"code": "de", "displayName": "Deutsch", "englishName": "German"}, {"code": "hu", "displayName": "Magyar", "englishName": "Hungarian"} ] } diff --git a/client/ui/frontend/src/i18n/locales/de/common.json b/client/ui/frontend/src/i18n/locales/de/common.json new file mode 100644 index 000000000..36ff3a487 --- /dev/null +++ b/client/ui/frontend/src/i18n/locales/de/common.json @@ -0,0 +1,34 @@ +{ + "tray.tooltip": "NetBird", + "tray.status.disconnected": "Getrennt", + "tray.status.daemonUnavailable": "Nicht aktiv", + "tray.status.error": "Fehler", + + "tray.menu.open": "NetBird öffnen", + "tray.menu.connect": "Verbinden", + "tray.menu.disconnect": "Trennen", + "tray.menu.exitNode": "Exit-Node", + "tray.menu.networks": "Ressourcen", + "tray.menu.profiles": "Profile", + "tray.menu.settings": "Einstellungen", + "tray.menu.debugBundle": "Debug-Paket erstellen", + "tray.menu.about": "Über", + "tray.menu.github": "GitHub", + "tray.menu.documentation": "Dokumentation", + "tray.menu.downloadLatest": "Neueste Version herunterladen", + "tray.menu.installVersion": "Version {version} installieren", + "tray.menu.guiVersion": "Oberfläche: {version}", + "tray.menu.daemonVersion": "Daemon: {version}", + "tray.menu.versionUnknown": "—", + "tray.menu.quit": "Beenden", + + "notify.update.title": "NetBird-Update verfügbar", + "notify.update.body": "NetBird {version} ist verfügbar.", + "notify.update.enforcedSuffix": " Ihr Administrator verlangt dieses Update.", + "notify.error.title": "Fehler", + "notify.error.connect": "Verbindung fehlgeschlagen", + "notify.error.disconnect": "Trennen fehlgeschlagen", + "notify.error.switchProfile": "Wechsel zu {profile} fehlgeschlagen", + "notify.sessionExpired.title": "NetBird-Sitzung abgelaufen", + "notify.sessionExpired.body": "Ihre NetBird-Sitzung ist abgelaufen. Bitte melden Sie sich erneut an." +} diff --git a/client/ui/frontend/src/layouts/AppLayout.tsx b/client/ui/frontend/src/layouts/AppLayout.tsx index 4cc234942..19bce3241 100644 --- a/client/ui/frontend/src/layouts/AppLayout.tsx +++ b/client/ui/frontend/src/layouts/AppLayout.tsx @@ -1,23 +1,28 @@ +import { useState } from "react"; import { Outlet } from "react-router-dom"; import { Header } from "@/layouts/Header.tsx"; -import { AppearanceProvider } from "@/modules/appearance/AppearanceContext.tsx"; import { ClientVersionProvider } from "@/modules/auto-update/ClientVersionContext.tsx"; import { DebugBundleProvider } from "@/modules/debug-bundle/DebugBundleContext.tsx"; import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx"; +// The wide-panel toggle lives in plain React state here so every launch +// starts in the small layout — no localStorage, no cross-machine drift. +// Header drives the toggle; Main reads it via Outlet context to decide +// whether to mount the right-side panel. +export type MainOutletContext = { expanded: boolean }; + export const AppLayout = () => { + const [expanded, setExpanded] = useState(false); return (
- - - - -
- - - - - + + + +
+ + + +
); }; diff --git a/client/ui/frontend/src/layouts/ConnectionStatus.tsx b/client/ui/frontend/src/layouts/ConnectionStatus.tsx deleted file mode 100644 index 395578ec3..000000000 --- a/client/ui/frontend/src/layouts/ConnectionStatus.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { ConnectionState, NetBirdConnectToggle } from "@/components/NetBirdConnectToggle.tsx"; -import Button from "@/components/Button.tsx"; -import { cn } from "@/lib/cn.ts"; - -const CONNECT_DURATION_MS = 1500; -const DISCONNECT_DURATION_MS = 800; - -const STATUS_LABEL: Record = { - [ConnectionState.Disconnected]: "Disconnected", - [ConnectionState.Connecting]: "Connecting...", - [ConnectionState.Connected]: "Connected", - [ConnectionState.Disconnecting]: "Disconnecting...", -}; - -export const ConnectionStatus = () => { - const [state, setState] = useState(ConnectionState.Disconnected); - const timerRef = useRef | null>(null); - - useEffect( - () => () => { - if (timerRef.current) clearTimeout(timerRef.current); - }, - [], - ); - - const transition = (next: ConnectionState, after: ConnectionState, delay: number) => { - if (timerRef.current) clearTimeout(timerRef.current); - setState(next); - timerRef.current = setTimeout(() => { - setState(after); - timerRef.current = null; - }, delay); - }; - - const connect = () => - transition(ConnectionState.Connecting, ConnectionState.Connected, CONNECT_DURATION_MS); - const disconnect = () => - transition( - ConnectionState.Disconnecting, - ConnectionState.Disconnected, - DISCONNECT_DURATION_MS, - ); - - const handleToggleClick = () => { - if (state === ConnectionState.Disconnected) connect(); - else if (state === ConnectionState.Connected) disconnect(); - }; - - const handleButtonClick = () => { - if (state === ConnectionState.Disconnected) { - connect(); - return; - } - if (state === ConnectionState.Connected) { - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - setState(ConnectionState.Disconnected); - } - }; - - const isTransitioning = - state === ConnectionState.Connecting || state === ConnectionState.Disconnecting; - const isConnectedSide = - state === ConnectionState.Connected || state === ConnectionState.Disconnecting; - - const buttonLabel = isConnectedSide ? "Disconnect" : "Connect"; - const buttonVariant = isConnectedSide ? "secondary" : "primary"; - - return ( -
-
-
-

- peer-hostname.netbird.cloud -

-

- 192.168.0.1 -

-
- -
-

- {STATUS_LABEL[state]} -

-
- -
-
-
-
- ); -}; diff --git a/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx b/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx index d4868f813..b6b9b7a34 100644 --- a/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx +++ b/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx @@ -1,13 +1,19 @@ import { useMemo, useState } from "react"; import { Dialogs, Events } from "@wailsio/runtime"; import { Connection, WindowManager } from "@bindings/services"; -import { ConnectionState } from "@/components/NetBirdConnectToggle.tsx"; import { ToggleSwitch } from "@/components/ToggleSwitch.tsx"; import { useStatus } from "@/hooks/useStatus"; import { useProfile } from "@/modules/profile/ProfileContext.tsx"; import { cn } from "@/lib/cn.ts"; import netbirdFullLogo from "@/assets/logos/netbird-full.svg"; +enum ConnectionState { + Disconnected = "disconnected", + Connecting = "connecting", + Connected = "connected", + Disconnecting = "disconnecting", +} + const STATUS_LABEL: Record = { [ConnectionState.Disconnected]: "Disconnected", [ConnectionState.Connecting]: "Connecting...", diff --git a/client/ui/frontend/src/layouts/Header.tsx b/client/ui/frontend/src/layouts/Header.tsx index 3e91aca9b..209bf39ad 100644 --- a/client/ui/frontend/src/layouts/Header.tsx +++ b/client/ui/frontend/src/layouts/Header.tsx @@ -4,7 +4,6 @@ import { Window } from "@wailsio/runtime"; import { WindowManager } from "@bindings/services"; import { ProfileSelector } from "@/components/ProfileSelector.tsx"; import { IconButton } from "@/components/IconButton.tsx"; -import { useAppearance } from "@/modules/appearance/AppearanceContext.tsx"; import { cn } from "@/lib/cn"; const WINDOW_SMALL_WIDTH = 380; @@ -12,8 +11,12 @@ const WINDOW_BIG_WIDTH = 925; const WINDOW_HEIGHT = 615; const EXPANDED_THRESHOLD = 500; -export const Header = () => { - const { showProfileSelector, showSettingsButton, expanded, setField } = useAppearance(); +type HeaderProps = { + expanded: boolean; + setExpanded: (next: boolean) => void; +}; + +export const Header = ({ expanded, setExpanded }: HeaderProps) => { const didInitialResize = useRef(false); useEffect(() => { @@ -33,15 +36,15 @@ export const Header = () => { useEffect(() => { const onResize = () => { const isWide = window.innerWidth >= EXPANDED_THRESHOLD; - if (isWide !== expanded) setField("expanded", isWide); + if (isWide !== expanded) setExpanded(isWide); }; window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); - }, [expanded, setField]); + }, [expanded, setExpanded]); const togglePanel = () => { const next = !expanded; - setField("expanded", next); + setExpanded(next); const w = next ? WINDOW_BIG_WIDTH : WINDOW_SMALL_WIDTH; void Window.SetSize(w, WINDOW_HEIGHT).catch(() => {}); }; @@ -57,19 +60,15 @@ export const Header = () => { "pt-4", )} > - {showProfileSelector && ( -
- -
- )} +
+ +
- {showSettingsButton && ( - - )} +
); }; diff --git a/client/ui/frontend/src/layouts/Main.tsx b/client/ui/frontend/src/layouts/Main.tsx index 861005edb..acb5a97dd 100644 --- a/client/ui/frontend/src/layouts/Main.tsx +++ b/client/ui/frontend/src/layouts/Main.tsx @@ -1,13 +1,13 @@ -import { ConnectionStatus } from "@/layouts/ConnectionStatus.tsx"; +import { useOutletContext } from "react-router-dom"; import { ConnectionStatusSwitch } from "@/layouts/ConnectionStatusSwitch.tsx"; import { MainRightSide } from "@/layouts/MainRightSide.tsx"; import { Navigation } from "@/layouts/Navigation.tsx"; import { Peers } from "@/modules/peers/Peers.tsx"; -import { useAppearance } from "@/modules/appearance/AppearanceContext.tsx"; +import type { MainOutletContext } from "@/layouts/AppLayout.tsx"; import { cn } from "@/lib/cn"; export const Main = () => { - const { connectionLayout, expanded } = useAppearance(); + const { expanded } = useOutletContext(); return (
{ expanded && "max-w-xs", )} > - {connectionLayout === "switch" ? ( - - ) : ( - - )} +
{expanded && ( diff --git a/client/ui/frontend/src/layouts/Navigation.tsx b/client/ui/frontend/src/layouts/Navigation.tsx index 646b4b7e1..eae520014 100644 --- a/client/ui/frontend/src/layouts/Navigation.tsx +++ b/client/ui/frontend/src/layouts/Navigation.tsx @@ -1,7 +1,6 @@ import { CardNavItem } from "@/components/CardNavItem.tsx"; import { Layers3Icon, MonitorSmartphoneIcon } from "lucide-react"; import deFlag from "@/assets/flags/1x1/de.svg"; -import { useAppearance } from "@/modules/appearance/AppearanceContext.tsx"; type Props = { peersActive?: boolean; @@ -9,42 +8,34 @@ type Props = { }; export const Navigation = ({ peersActive = false, onPeersClick }: Props) => { - const { showPeersNav, showResourcesNav, showExitNodeNav } = useAppearance(); - return ( ); }; diff --git a/client/ui/frontend/src/layouts/SettingsLayout.tsx b/client/ui/frontend/src/layouts/SettingsLayout.tsx index e56dcd6fe..ce6c5e50e 100644 --- a/client/ui/frontend/src/layouts/SettingsLayout.tsx +++ b/client/ui/frontend/src/layouts/SettingsLayout.tsx @@ -1,5 +1,4 @@ import { Outlet } from "react-router-dom"; -import { AppearanceProvider } from "@/modules/appearance/AppearanceContext.tsx"; import { ClientVersionProvider } from "@/modules/auto-update/ClientVersionContext.tsx"; import { DebugBundleProvider } from "@/modules/debug-bundle/DebugBundleContext.tsx"; import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx"; @@ -10,27 +9,26 @@ import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx"; // selector / panel toggle / settings icon. // // The 38px placeholder strip at the top accounts for the macOS -// `MacTitleBarHiddenInset` setting in services/windows.go: the native title -// bar is invisible but the traffic-light buttons still float in the top-left -// corner. Without this strip the buttons would overlap the settings content. -// The strip is `wails-draggable` so users can move the window by dragging it. +// `MacTitleBarHiddenInset` setting in services/windowmanager.go: the native +// title bar is invisible but the traffic-light buttons still float in the +// top-left corner. Without this strip the buttons would overlap the settings +// content. The strip is `wails-draggable` so users can move the window by +// dragging it. export const SettingsLayout = () => { return (
- - - - -
- - - - - + + + +
+ + + +
); }; diff --git a/client/ui/frontend/src/modules/appearance/AppearanceContext.tsx b/client/ui/frontend/src/modules/appearance/AppearanceContext.tsx deleted file mode 100644 index 2414954d3..000000000 --- a/client/ui/frontend/src/modules/appearance/AppearanceContext.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useState, - type ReactNode, -} from "react"; - -export type ConnectionLayout = "default" | "switch"; - -export type AppearanceState = { - connectionLayout: ConnectionLayout; - expanded: boolean; - showPeersNav: boolean; - showResourcesNav: boolean; - showExitNodeNav: boolean; - showProfileSelector: boolean; - showSettingsButton: boolean; -}; - -const STORAGE_KEY = "netbird:appearance"; - -const DEFAULTS: AppearanceState = { - connectionLayout: "default", - expanded: true, - showPeersNav: true, - showResourcesNav: true, - showExitNodeNav: true, - showProfileSelector: true, - showSettingsButton: true, -}; - -const readStored = (): AppearanceState => { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return DEFAULTS; - const parsed = JSON.parse(raw) as Partial; - return { ...DEFAULTS, ...parsed }; - } catch { - return DEFAULTS; - } -}; - -type AppearanceContextValue = AppearanceState & { - setField: (k: K, v: AppearanceState[K]) => void; -}; - -const AppearanceContext = createContext(null); - -export const useAppearance = () => { - const ctx = useContext(AppearanceContext); - if (!ctx) { - throw new Error("useAppearance must be used inside AppearanceProvider"); - } - return ctx; -}; - -export const AppearanceProvider = ({ children }: { children: ReactNode }) => { - const [state, setState] = useState(() => readStored()); - - useEffect(() => { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); - } catch { - // ignore quota / unavailable storage - } - }, [state]); - - const setField = useCallback( - (k: K, v: AppearanceState[K]) => { - setState((s) => ({ ...s, [k]: v })); - }, - [], - ); - - const value = useMemo( - () => ({ ...state, setField }), - [state, setField], - ); - - return ( - {children} - ); -}; diff --git a/client/ui/frontend/src/modules/settings/Settings.tsx b/client/ui/frontend/src/modules/settings/Settings.tsx index 176625119..4cebdd55d 100644 --- a/client/ui/frontend/src/modules/settings/Settings.tsx +++ b/client/ui/frontend/src/modules/settings/Settings.tsx @@ -7,7 +7,6 @@ import { VerticalTabs } from "@/components/VerticalTabs.tsx"; import { SettingsNavigationTriggers } from "@/modules/settings/SettingsNavigationTriggers.tsx"; import { SettingsProvider } from "@/modules/settings/SettingsContext.tsx"; import { SettingsGeneral } from "@/modules/settings/SettingsGeneral.tsx"; -import { SettingsAppearance } from "@/modules/settings/SettingsAppearance.tsx"; import { SettingsNetwork } from "@/modules/settings/SettingsNetwork.tsx"; import { SettingsSecurity } from "@/modules/settings/SettingsSecurity.tsx"; import { SettingsSSH } from "@/modules/settings/SettingsSSH.tsx"; @@ -58,9 +57,6 @@ export const Settings = () => { - - - diff --git a/client/ui/frontend/src/modules/settings/SettingsAppearance.tsx b/client/ui/frontend/src/modules/settings/SettingsAppearance.tsx deleted file mode 100644 index 62b49cee1..000000000 --- a/client/ui/frontend/src/modules/settings/SettingsAppearance.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import FancyToggleSwitch from "@/components/FancyToggleSwitch"; -import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; -import { useAppearance } from "@/modules/appearance/AppearanceContext.tsx"; - -export function SettingsAppearance() { - const { - showPeersNav, - showResourcesNav, - showExitNodeNav, - showProfileSelector, - showSettingsButton, - setField, - } = useAppearance(); - - return ( - - setField("showPeersNav", v)} - label={"Peers"} - helpText={"Show the Peers item in the side navigation."} - /> - setField("showResourcesNav", v)} - label={"Resources"} - helpText={"Show the Resources item in the side navigation."} - /> - setField("showExitNodeNav", v)} - label={"Exit Node"} - helpText={"Show the active exit node in the side navigation."} - /> - setField("showProfileSelector", v)} - label={"Profile Selector"} - helpText={"Show the profile selector in the header."} - /> - setField("showSettingsButton", v)} - label={"Settings Button"} - helpText={"Show the settings button in the header."} - /> - - ); -} diff --git a/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx b/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx index 97e160fa1..3a751beb1 100644 --- a/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx @@ -10,7 +10,6 @@ import { ShieldIcon, SlidersHorizontalIcon, SquareTerminalIcon, - SwatchBookIcon, } from "lucide-react"; export const SettingsNavigationTriggers = () => { @@ -30,11 +29,6 @@ export const SettingsNavigationTriggers = () => { icon={SlidersHorizontalIcon} title={"General"} /> - navigate("/login"); - const connect = () => Connection.Up({ profileName: "", username: "" }).catch(console.error); - const disconnect = () => Connection.Down().catch(console.error); - const toggleConnection = () => { - if (needsLogin) { - navigate("/login"); - return; - } - if (connected) { - disconnect(); - return; - } - connect(); - }; - - return ( -
-
-
- -
-

{connState}

-

- {status?.local.fqdn || "—"} -

-
-
-
- {needsLogin ? ( - - ) : ( - - )} - {showLogin && !needsLogin && ( - - )} - -
-
- - {error && ( -
- - {error} -
- )} - -
- - - - -
- - -

- Recent events -

- {(() => { - const events = dedupEvents(status?.events ?? []).slice(0, 8); - if (events.length === 0) { - return

No recent events.

; - } - return ( -
    - {events.map((e, i) => ( -
  • - - {e.severity} - - - {e.userMessage || e.message} - -
  • - ))} -
- ); - })()} -
- -
- -
-
- ); -} - -function StateIcon({ state }: { state: string }) { - const cls = "h-7 w-7"; - switch (state) { - case "Connected": - return ; - case "Connecting": - return ; - case "Error": - return ; - default: - return ; - } -} - -function InfoCard({ label, value }: { label: string; value: string }) { - return ( - -

{label}

-

{value}

-
- ); -} - -// dedupEvents collapses repeated daemon events that carry the same logical -// content. The daemon emits one "new_version_available" event per check tick, -// so its 10-event ring buffer fills with duplicates after a quiet hour. Same -// goes for periodic "DNS unreachable" or "auth retry" events. We key by -// message + a small set of identity-bearing metadata fields and keep the -// newest occurrence (the events array is already in publish order). -function dedupEvents(events: SystemEvent[]): SystemEvent[] { - const seen = new Set(); - const out: SystemEvent[] = []; - for (let i = events.length - 1; i >= 0; i--) { - const e = events[i]; - const md = e.metadata ?? {}; - const key = [ - e.severity, - e.category, - e.userMessage || e.message, - md["new_version_available"] ?? "", - md["enforced"] ?? "", - ].join("|"); - // eslint-disable-next-line no-console - console.log("[dedup]", { key, event: e }); - if (seen.has(key)) continue; - seen.add(key); - out.unshift(e); - } - return out; -} - -function LinkCard({ - label, - link, -}: { - label: string; - link?: { url: string; connected: boolean; error?: string }; -}) { - return ( - -
-

{label}

- -
-

- {link?.url || "—"} -

- {link?.error && ( -

{link.error}

- )} -
- ); -} diff --git a/client/ui/main.go b/client/ui/main.go index ec444d0da..da05da920 100644 --- a/client/ui/main.go +++ b/client/ui/main.go @@ -162,9 +162,14 @@ func main() { app.RegisterService(application.NewService(services.NewI18n(bundle))) app.RegisterService(application.NewService(services.NewPreferences(prefStore))) + // Initial size matches AppearanceContext's default `expanded: false` + // (small / simple view). When the user has previously expanded the + // window, Header.tsx's mount effect resizes back up to 925 via + // Window.SetSize — that's a one-shot grow rather than a shrink, which + // reads better than starting wide and snapping narrow on every launch. window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "NetBird", - Width: 925, + Width: 380, Height: 615, Hidden: true, BackgroundColour: application.NewRGB(24, 26, 29),